commit 7821596c57540ff35ddab2a64bff5aad617ac4ef Author: Logen Kain Date: Fri Oct 9 21:13:59 2020 -0400 init diff --git a/notes.lisp b/notes.lisp new file mode 100644 index 0000000..3905864 --- /dev/null +++ b/notes.lisp @@ -0,0 +1,143 @@ +rlwrap -- helps deal with readline +rlwrap sbcl -- makes it so I have history and navigation + +Functions shoudl be verbs -- clear-screen +variables should be nouns -- dog +globals should be surround with asterisks -- +*some-global-var* +It should be rare to have more than one expression in a function. + +Instead of using variables, like we do in other languages, we should instead nest functions. + +For exampe, in python we may do something like this: + + +;;; Python +def addTwoNumbers(x, y): + sum = x + y + return sum + +sum = addTwoNumbers(1, 3) +print(sum) + +;;; Lisp + +(defun addTwoNumbers(x y) + (+ x y)) + +(print + (addTwoNumbers(1 3))) + +Of course, the python one could simply be "return x + y" but the point is to +show the normal differences in thought, not to say that it's not possible +to do it in a lispy way in python. + +To copy an example in only lisp to show the differenes from +cs.gmu.edu/~sean/lisp/LispTutorial.html + +;;; Declaritive style (I.E. the python way above) + +(defun my-equation (n) + (let (x y z) + (setf x (sin n)) + (setf y (cos n)) + (setf z (* x y)) + (+ n z))) + +;;; Functional style + +(defun my-equation (n) + (+ n (* (sin n) (cos n)))) + +As we can see, we avoid variables and make it look pretty + + +;; Closure example: +;; So what happens here? All of these functions get to share the "account" var +;; yet, "account" is not asseble outside the functions. +;; Since the functions are there, the let statement doesn't get garbage +;; collected? +;; In a manner of speaking, defining functions within a let statement +;; allows for a private global variable + +;; not much different from a java or C++ object + +(let ((account 0)) + (defun deposit ($$$) + (setf account (+ account $$$))) + (defun withdraw ($$$) + (setf account (- account $$$))) + (defun amount () + account)) + +;; grabbing an element from a sequence (i.e. not multi-dimentional array) + +;; Generic +(elt "hello world" 4) +(elt '(yo 1 3 4) 2) +(elt #(yo yo dur cob so 2 3) 3) + +;; String +(aref "hi there" 3) + +;; list +(nth 3 '(1 2 3 4 5 6 7)) + +;; simple-vector +(svref #(d 1 3 f g e) 4) + +;; P-Lists (property lists) + +(setf my-list '(armor (head + (dragon helmet) + legs () + arms ()) + weapon (left + (wooden-shield) + right ()) + ring (left + () + right ()))) + +(getf my-list 'armor) >> (HEAD LEGS ARMS) +(getf (getf my-list 'armor) 'head) + +;; Let's make it less crazy +(defun equipted-helmet (my-list) + (getf (getf my-list 'armor) 'head)) + +;; doing something like (setf (equipted-helmet my-list) 'hammer) doesn't +;; seem to work. Perhaps a closure would be better + +(let ((equipment '(armor (head + (dragon helmet) + legs () + arms ()) + weapon (left + (wooden-shield) + right ()) + ring (left + () + right ())))) + (defun get-armor() + (getf equipment 'armor)) + (defun get-helmet() + (getf (get-armor) 'head)) + (defun change-helmet () + (push (getf (getf equipment 'armor) 'head) x)) + ;;(setf (getf (getf equipment 'armor) 'head) x)) + #| Etc... |# ) + +Why doesn't this work? + +(defvar mylist '(j 1 v 2)) + +;; Works +(setf (getf mylist 'j) 3) + +(defun get-j () + (getf mylist `j)) + +;;doesn't work +(setf (get-j) 'j) + diff --git a/quicklisp.lisp b/quicklisp.lisp new file mode 100644 index 0000000..6cda472 --- /dev/null +++ b/quicklisp.lisp @@ -0,0 +1,1757 @@ +;;;; +;;;; This is quicklisp.lisp, the quickstart file for Quicklisp. To use +;;;; it, start Lisp, then (load "quicklisp.lisp") +;;;; +;;;; Quicklisp is beta software and comes with no warranty of any kind. +;;;; +;;;; For more information about the Quicklisp beta, see: +;;;; +;;;; http://www.quicklisp.org/beta/ +;;;; +;;;; If you have any questions or comments about Quicklisp, please +;;;; contact: +;;;; +;;;; Zach Beane +;;;; + +(cl:in-package #:cl-user) +(cl:defpackage #:qlqs-user + (:use #:cl)) +(cl:in-package #:qlqs-user) + +(defpackage #:qlqs-info + (:export #:*version*)) + +(defvar qlqs-info:*version* "2015-01-28") + +(defpackage #:qlqs-impl + (:use #:cl) + (:export #:*implementation*) + (:export #:definterface + #:defimplementation) + (:export #:lisp + #:abcl + #:allegro + #:ccl + #:clasp + #:clisp + #:cmucl + #:cormanlisp + #:ecl + #:gcl + #:lispworks + #:mkcl + #:scl + #:sbcl)) + +(defpackage #:qlqs-impl-util + (:use #:cl #:qlqs-impl) + (:export #:call-with-quiet-compilation)) + +(defpackage #:qlqs-network + (:use #:cl #:qlqs-impl) + (:export #:open-connection + #:write-octets + #:read-octets + #:close-connection + #:with-connection)) + +(defpackage #:qlqs-progress + (:use #:cl) + (:export #:make-progress-bar + #:start-display + #:update-progress + #:finish-display)) + +(defpackage #:qlqs-http + (:use #:cl #:qlqs-network #:qlqs-progress) + (:export #:fetch + #:*proxy-url* + #:*maximum-redirects* + #:*default-url-defaults*)) + +(defpackage #:qlqs-minitar + (:use #:cl) + (:export #:unpack-tarball)) + +(defpackage #:quicklisp-quickstart + (:use #:cl #:qlqs-impl #:qlqs-impl-util #:qlqs-http #:qlqs-minitar) + (:export #:install + #:help + #:*proxy-url* + #:*asdf-url* + #:*quicklisp-tar-url* + #:*setup-url* + #:*help-message* + #:*after-load-message* + #:*after-initial-setup-message*)) + + +;;; +;;; Defining implementation-specific packages and functionality +;;; + +(in-package #:qlqs-impl) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (defun error-unimplemented (&rest args) + (declare (ignore args)) + (error "Not implemented"))) + +(defmacro neuter-package (name) + `(eval-when (:compile-toplevel :load-toplevel :execute) + (let ((definition (fdefinition 'error-unimplemented))) + (do-external-symbols (symbol ,(string name)) + (unless (fboundp symbol) + (setf (fdefinition symbol) definition)))))) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (defun feature-expression-passes-p (expression) + (cond ((keywordp expression) + (member expression *features*)) + ((consp expression) + (case (first expression) + (or + (some 'feature-expression-passes-p (rest expression))) + (and + (every 'feature-expression-passes-p (rest expression))))) + (t (error "Unrecognized feature expression -- ~S" expression))))) + + +(defmacro define-implementation-package (feature package-name &rest options) + (let* ((output-options '((:use) + (:export #:lisp))) + (prep (cdr (assoc :prep options))) + (class-option (cdr (assoc :class options))) + (class (first class-option)) + (superclasses (rest class-option)) + (import-options '()) + (effectivep (feature-expression-passes-p feature))) + (dolist (option options) + (ecase (first option) + ((:prep :class)) + ((:import-from + :import) + (push option import-options)) + ((:export + :shadow + :intern + :documentation) + (push option output-options)) + ((:reexport-from) + (push (cons :export (cddr option)) output-options) + (push (cons :import-from (cdr option)) import-options)))) + `(eval-when (:compile-toplevel :load-toplevel :execute) + ,@(when effectivep + prep) + (defclass ,class ,superclasses ()) + (defpackage ,package-name ,@output-options + ,@(when effectivep + import-options)) + ,@(when effectivep + `((setf *implementation* (make-instance ',class)))) + ,@(unless effectivep + `((neuter-package ,package-name)))))) + +(defmacro definterface (name lambda-list &body options) + (let* ((forbidden (intersection lambda-list lambda-list-keywords)) + (gf-options (remove :implementation options :key #'first)) + (implementations (set-difference options gf-options))) + (when forbidden + (error "~S not allowed in definterface lambda list" forbidden)) + (flet ((method-option (class body) + `(:method ((*implementation* ,class) ,@lambda-list) + ,@body))) + (let ((generic-name (intern (format nil "%~A" name)))) + `(eval-when (:compile-toplevel :load-toplevel :execute) + (defgeneric ,generic-name (lisp ,@lambda-list) + ,@gf-options + ,@(mapcar (lambda (implementation) + (destructuring-bind (class &rest body) + (rest implementation) + (method-option class body))) + implementations)) + (defun ,name ,lambda-list + (,generic-name *implementation* ,@lambda-list))))))) + +(defmacro defimplementation (name-and-options + lambda-list &body body) + (destructuring-bind (name &key (for t) qualifier) + (if (consp name-and-options) + name-and-options + (list name-and-options)) + (unless for + (error "You must specify an implementation name.")) + (let ((generic-name (find-symbol (format nil "%~A" name)))) + (unless (and generic-name + (fboundp generic-name)) + (error "~S does not name an implementation function" name)) + `(defmethod ,generic-name + ,@(when qualifier (list qualifier)) + ,(list* `(*implementation* ,for) lambda-list) ,@body)))) + + +;;; Bootstrap implementations + +(defvar *implementation* nil) +(defclass lisp () ()) + + +;;; Allegro Common Lisp + +(define-implementation-package :allegro #:qlqs-allegro + (:documentation + "Allegro Common Lisp - http://www.franz.com/products/allegrocl/") + (:class allegro) + (:reexport-from #:socket + #:make-socket) + (:reexport-from #:excl + #:read-vector)) + + +;;; Armed Bear Common Lisp + +(define-implementation-package :abcl #:qlqs-abcl + (:documentation + "Armed Bear Common Lisp - http://common-lisp.net/project/armedbear/") + (:class abcl) + (:reexport-from #:system + #:make-socket + #:get-socket-stream)) + +;;; Clozure CL + +(define-implementation-package :ccl #:qlqs-ccl + (:documentation + "Clozure Common Lisp - http://www.clozure.com/clozurecl.html") + (:class ccl) + (:reexport-from #:ccl + #:make-socket)) + + +;;; CLASP + +(define-implementation-package :clasp #:qlqs-clasp + (:documentation "CLASP - http://github.com/drmeister/clasp") + (:class clasp) + (:prep + (require 'sockets)) + (:intern #:host-network-address) + (:reexport-from #:sb-bsd-sockets + #:get-host-by-name + #:host-ent-address + #:socket-connect + #:socket-make-stream + #:inet-socket)) + + +;;; GNU CLISP + +(define-implementation-package :clisp #:qlqs-clisp + (:documentation "GNU CLISP - http://clisp.cons.org/") + (:class clisp) + (:reexport-from #:socket + #:socket-connect) + (:reexport-from #:ext + #:read-byte-sequence)) + + +;;; CMUCL + +(define-implementation-package :cmu #:qlqs-cmucl + (:documentation "CMU Common Lisp - http://www.cons.org/cmucl/") + (:class cmucl) + (:reexport-from #:ext + #:*gc-verbose*) + (:reexport-from #:system + #:make-fd-stream) + (:reexport-from #:extensions + #:connect-to-inet-socket)) + +(defvar qlqs-cmucl:*gc-verbose* nil) + + +;;; Scieneer CL + +(define-implementation-package :scl #:qlqs-scl + (:documentation "Scieneer Common Lisp - http://www.scieneer.com/scl/") + (:class scl) + (:reexport-from #:system + #:make-fd-stream) + (:reexport-from #:extensions + #:connect-to-inet-socket)) + +;;; ECL + +(define-implementation-package :ecl #:qlqs-ecl + (:documentation "ECL - http://ecls.sourceforge.net/") + (:class ecl) + (:prep + (require 'sockets)) + (:intern #:host-network-address) + (:reexport-from #:sb-bsd-sockets + #:get-host-by-name + #:host-ent-address + #:socket-connect + #:socket-make-stream + #:inet-socket)) + + +;;; LispWorks + +(define-implementation-package :lispworks #:qlqs-lispworks + (:documentation "LispWorks - http://www.lispworks.com/") + (:class lispworks) + (:prep + (require "comm")) + (:reexport-from #:comm + #:open-tcp-stream + #:get-host-entry)) + + +;;; SBCL + +(define-implementation-package :sbcl #:qlqs-sbcl + (:class sbcl) + (:documentation + "Steel Bank Common Lisp - http://www.sbcl.org/") + (:prep + (require 'sb-bsd-sockets)) + (:intern #:host-network-address) + (:reexport-from #:sb-ext + #:compiler-note) + (:reexport-from #:sb-bsd-sockets + #:get-host-by-name + #:inet-socket + #:host-ent-address + #:socket-connect + #:socket-make-stream)) + +;;; MKCL + +(define-implementation-package :mkcl #:qlqs-mkcl + (:class mkcl) + (:documentation + "ManKai Common Lisp - http://common-lisp.net/project/mkcl/") + (:prep + (require 'sockets)) + (:intern #:host-network-address) + (:reexport-from #:sb-bsd-sockets + #:get-host-by-name + #:inet-socket + #:host-ent-address + #:socket-connect + #:socket-make-stream)) + +;;; +;;; Utility function +;;; + +(in-package #:qlqs-impl-util) + +(definterface call-with-quiet-compilation (fun) + (:implementation t + (let ((*load-verbose* nil) + (*compile-verbose* nil) + (*load-print* nil) + (*compile-print* nil)) + (handler-bind ((warning #'muffle-warning)) + (funcall fun))))) + +(defimplementation (call-with-quiet-compilation :for sbcl :qualifier :around) + (fun) + (declare (ignorable fun)) + (handler-bind ((qlqs-sbcl:compiler-note #'muffle-warning)) + (call-next-method))) + +(defimplementation (call-with-quiet-compilation :for cmucl :qualifier :around) + (fun) + (declare (ignorable fun)) + (let ((qlqs-cmucl:*gc-verbose* nil)) + (call-next-method))) + + +;;; +;;; Low-level networking implementations +;;; + +(in-package #:qlqs-network) + +(definterface host-address (host) + (:implementation t + host) + (:implementation mkcl + (qlqs-mkcl:host-ent-address (qlqs-mkcl:get-host-by-name host))) + (:implementation sbcl + (qlqs-sbcl:host-ent-address (qlqs-sbcl:get-host-by-name host)))) + +(definterface open-connection (host port) + (:implementation t + (declare (ignorable host port)) + (error "Sorry, quicklisp in implementation ~S is not supported yet." + (lisp-implementation-type))) + (:implementation allegro + (qlqs-allegro:make-socket :remote-host host + :remote-port port)) + (:implementation abcl + (let ((socket (qlqs-abcl:make-socket host port))) + (qlqs-abcl:get-socket-stream socket :element-type '(unsigned-byte 8)))) + (:implementation ccl + (qlqs-ccl:make-socket :remote-host host + :remote-port port)) + (:implementation clasp + (let* ((endpoint (qlqs-clasp:host-ent-address + (qlqs-clasp:get-host-by-name host))) + (socket (make-instance 'qlqs-clasp:inet-socket + :protocol :tcp + :type :stream))) + (qlqs-clasp:socket-connect socket endpoint port) + (qlqs-clasp:socket-make-stream socket + :element-type '(unsigned-byte 8) + :input t + :output t + :buffering :full))) + (:implementation clisp + (qlqs-clisp:socket-connect port host :element-type '(unsigned-byte 8))) + (:implementation cmucl + (let ((fd (qlqs-cmucl:connect-to-inet-socket host port))) + (qlqs-cmucl:make-fd-stream fd + :element-type '(unsigned-byte 8) + :binary-stream-p t + :input t + :output t))) + (:implementation scl + (let ((fd (qlqs-scl:connect-to-inet-socket host port))) + (qlqs-scl:make-fd-stream fd + :element-type '(unsigned-byte 8) + :input t + :output t))) + (:implementation ecl + (let* ((endpoint (qlqs-ecl:host-ent-address + (qlqs-ecl:get-host-by-name host))) + (socket (make-instance 'qlqs-ecl:inet-socket + :protocol :tcp + :type :stream))) + (qlqs-ecl:socket-connect socket endpoint port) + (qlqs-ecl:socket-make-stream socket + :element-type '(unsigned-byte 8) + :input t + :output t + :buffering :full))) + (:implementation lispworks + (qlqs-lispworks:open-tcp-stream host port + :direction :io + :errorp t + :read-timeout nil + :element-type '(unsigned-byte 8) + :timeout 5)) + (:implementation mkcl + (let* ((endpoint (qlqs-mkcl:host-ent-address + (qlqs-mkcl:get-host-by-name host))) + (socket (make-instance 'qlqs-mkcl:inet-socket + :protocol :tcp + :type :stream))) + (qlqs-mkcl:socket-connect socket endpoint port) + (qlqs-mkcl:socket-make-stream socket + :element-type '(unsigned-byte 8) + :input t + :output t + :buffering :full))) + (:implementation sbcl + (let* ((endpoint (qlqs-sbcl:host-ent-address + (qlqs-sbcl:get-host-by-name host))) + (socket (make-instance 'qlqs-sbcl:inet-socket + :protocol :tcp + :type :stream))) + (qlqs-sbcl:socket-connect socket endpoint port) + (qlqs-sbcl:socket-make-stream socket + :element-type '(unsigned-byte 8) + :input t + :output t + :buffering :full)))) + +(definterface read-octets (buffer connection) + (:implementation t + (read-sequence buffer connection)) + (:implementation allegro + (qlqs-allegro:read-vector buffer connection)) + (:implementation clisp + (qlqs-clisp:read-byte-sequence buffer connection + :no-hang nil + :interactive t))) + +(definterface write-octets (buffer connection) + (:implementation t + (write-sequence buffer connection) + (finish-output connection))) + +(definterface close-connection (connection) + (:implementation t + (ignore-errors (close connection)))) + +(definterface call-with-connection (host port fun) + (:implementation t + (let (connection) + (unwind-protect + (progn + (setf connection (open-connection host port)) + (funcall fun connection)) + (when connection + (close connection)))))) + +(defmacro with-connection ((connection host port) &body body) + `(call-with-connection ,host ,port (lambda (,connection) ,@body))) + + +;;; +;;; A text progress bar +;;; + +(in-package #:qlqs-progress) + +(defclass progress-bar () + ((start-time + :initarg :start-time + :accessor start-time) + (end-time + :initarg :end-time + :accessor end-time) + (progress-character + :initarg :progress-character + :accessor progress-character) + (character-count + :initarg :character-count + :accessor character-count + :documentation "How many characters wide is the progress bar?") + (characters-so-far + :initarg :characters-so-far + :accessor characters-so-far) + (update-interval + :initarg :update-interval + :accessor update-interval + :documentation "Update the progress bar display after this many + internal-time units.") + (last-update-time + :initarg :last-update-time + :accessor last-update-time + :documentation "The display was last updated at this time.") + (total + :initarg :total + :accessor total + :documentation "The total number of units tracked by this progress bar.") + (progress + :initarg :progress + :accessor progress + :documentation "How far in the progress are we?") + (pending + :initarg :pending + :accessor pending + :documentation "How many raw units should be tracked in the next + display update?")) + (:default-initargs + :progress-character #\= + :character-count 50 + :characters-so-far 0 + :update-interval (floor internal-time-units-per-second 4) + :last-update-time 0 + :total 0 + :progress 0 + :pending 0)) + +(defgeneric start-display (progress-bar)) +(defgeneric update-progress (progress-bar unit-count)) +(defgeneric update-display (progress-bar)) +(defgeneric finish-display (progress-bar)) +(defgeneric elapsed-time (progress-bar)) +(defgeneric units-per-second (progress-bar)) + +(defmethod start-display (progress-bar) + (setf (last-update-time progress-bar) (get-internal-real-time)) + (setf (start-time progress-bar) (get-internal-real-time)) + (fresh-line) + (finish-output)) + +(defmethod update-display (progress-bar) + (incf (progress progress-bar) (pending progress-bar)) + (setf (pending progress-bar) 0) + (setf (last-update-time progress-bar) (get-internal-real-time)) + (let* ((showable (floor (character-count progress-bar) + (/ (total progress-bar) (progress progress-bar)))) + (needed (- showable (characters-so-far progress-bar)))) + (setf (characters-so-far progress-bar) showable) + (dotimes (i needed) + (write-char (progress-character progress-bar))) + (finish-output))) + +(defmethod update-progress (progress-bar unit-count) + (incf (pending progress-bar) unit-count) + (let ((now (get-internal-real-time))) + (when (< (update-interval progress-bar) + (- now (last-update-time progress-bar))) + (update-display progress-bar)))) + +(defmethod finish-display (progress-bar) + (update-display progress-bar) + (setf (end-time progress-bar) (get-internal-real-time)) + (terpri) + (format t "~:D bytes in ~$ seconds (~$KB/sec)" + (total progress-bar) + (elapsed-time progress-bar) + (/ (units-per-second progress-bar) 1024)) + (finish-output)) + +(defmethod elapsed-time (progress-bar) + (/ (- (end-time progress-bar) (start-time progress-bar)) + internal-time-units-per-second)) + +(defmethod units-per-second (progress-bar) + (if (plusp (elapsed-time progress-bar)) + (/ (total progress-bar) (elapsed-time progress-bar)) + 0)) + +(defun kb/sec (progress-bar) + (/ (units-per-second progress-bar) 1024)) + + + +(defparameter *uncertain-progress-chars* "?") + +(defclass uncertain-size-progress-bar (progress-bar) + ((progress-char-index + :initarg :progress-char-index + :accessor progress-char-index) + (units-per-char + :initarg :units-per-char + :accessor units-per-char)) + (:default-initargs + :total 0 + :progress-char-index 0 + :units-per-char (floor (expt 1024 2) 50))) + +(defmethod update-progress :after ((progress-bar uncertain-size-progress-bar) + unit-count) + (incf (total progress-bar) unit-count)) + +(defmethod progress-character ((progress-bar uncertain-size-progress-bar)) + (let ((index (progress-char-index progress-bar))) + (prog1 + (char *uncertain-progress-chars* index) + (setf (progress-char-index progress-bar) + (mod (1+ index) (length *uncertain-progress-chars*)))))) + +(defmethod update-display ((progress-bar uncertain-size-progress-bar)) + (setf (last-update-time progress-bar) (get-internal-real-time)) + (multiple-value-bind (chars pend) + (floor (pending progress-bar) (units-per-char progress-bar)) + (setf (pending progress-bar) pend) + (dotimes (i chars) + (write-char (progress-character progress-bar)) + (incf (characters-so-far progress-bar)) + (when (<= (character-count progress-bar) + (characters-so-far progress-bar)) + (terpri) + (setf (characters-so-far progress-bar) 0) + (finish-output))) + (finish-output))) + +(defun make-progress-bar (total) + (if (or (not total) (zerop total)) + (make-instance 'uncertain-size-progress-bar) + (make-instance 'progress-bar :total total))) + +;;; +;;; A simple HTTP client +;;; + +(in-package #:qlqs-http) + +;;; Octet data + +(deftype octet () + '(unsigned-byte 8)) + +(defun make-octet-vector (size) + (make-array size :element-type 'octet + :initial-element 0)) + +(defun octet-vector (&rest octets) + (make-array (length octets) :element-type 'octet + :initial-contents octets)) + +;;; ASCII characters as integers + +(defun acode (char) + (cond ((eql char :cr) + 13) + ((eql char :lf) + 10) + (t + (let ((code (char-code char))) + (if (<= 0 code 127) + code + (error "Character ~S is not in the ASCII character set" + char)))))) + +(defvar *whitespace* + (list (acode #\Space) (acode #\Tab) (acode :cr) (acode :lf))) + +(defun whitep (code) + (member code *whitespace*)) + +(defun ascii-vector (string) + (let ((vector (make-octet-vector (length string)))) + (loop for char across string + for code = (char-code char) + for i from 0 + if (< 127 code) do + (error "Invalid character for ASCII -- ~A" char) + else + do (setf (aref vector i) code)) + vector)) + +(defun ascii-subseq (vector start end) + "Return a subseq of octet-specialized VECTOR as a string." + (let ((string (make-string (- end start)))) + (loop for i from 0 + for j from start below end + do (setf (char string i) (code-char (aref vector j)))) + string)) + +(defun ascii-downcase (code) + (if (<= 65 code 90) + (+ code 32) + code)) + +(defun ascii-equal (a b) + (eql (ascii-downcase a) (ascii-downcase b))) + +(defmacro acase (value &body cases) + (flet ((convert-case-keys (keys) + (mapcar (lambda (key) + (etypecase key + (integer key) + (character (char-code key)) + (symbol + (ecase key + (:cr 13) + (:lf 10) + ((t) t))))) + (if (consp keys) keys (list keys))))) + `(case ,value + ,@(mapcar (lambda (case) + (destructuring-bind (keys &rest body) + case + `(,(if (eql keys t) + t + (convert-case-keys keys)) + ,@body))) + cases)))) + +;;; Pattern matching (for finding headers) + +(defclass matcher () + ((pattern + :initarg :pattern + :reader pattern) + (pos + :initform 0 + :accessor match-pos) + (matchedp + :initform nil + :accessor matchedp))) + +(defun reset-match (matcher) + (setf (match-pos matcher) 0 + (matchedp matcher) nil)) + +(define-condition match-failure (error) ()) + +(defun match (matcher input &key (start 0) end error) + (let ((i start) + (end (or end (length input))) + (match-end (length (pattern matcher)))) + (with-slots (pattern pos) + matcher + (loop + (cond ((= pos match-end) + (let ((match-start (- i pos))) + (setf pos 0) + (setf (matchedp matcher) t) + (return (values match-start (+ match-start match-end))))) + ((= i end) + (return nil)) + ((= (aref pattern pos) + (aref input i)) + (incf i) + (incf pos)) + (t + (if error + (error 'match-failure) + (if (zerop pos) + (incf i) + (setf pos 0))))))))) + +(defun ascii-matcher (string) + (make-instance 'matcher + :pattern (ascii-vector string))) + +(defun octet-matcher (&rest octets) + (make-instance 'matcher + :pattern (apply 'octet-vector octets))) + +(defun acode-matcher (&rest codes) + (make-instance 'matcher + :pattern (make-array (length codes) + :element-type 'octet + :initial-contents + (mapcar 'acode codes)))) + + +;;; "Connection Buffers" are a kind of callback-driven, +;;; pattern-matching chunky stream. Callbacks can be called for a +;;; certain number of octets or until one or more patterns are seen in +;;; the input. cbufs automatically refill themselves from a +;;; connection as needed. + +(defvar *cbuf-buffer-size* 8192) + +(define-condition end-of-data (error) ()) + +(defclass cbuf () + ((data + :initarg :data + :accessor data) + (connection + :initarg :connection + :accessor connection) + (start + :initarg :start + :accessor start) + (end + :initarg :end + :accessor end) + (eofp + :initarg :eofp + :accessor eofp)) + (:default-initargs + :data (make-octet-vector *cbuf-buffer-size*) + :connection nil + :start 0 + :end 0 + :eofp nil) + (:documentation "A CBUF is a connection buffer that keeps track of + incoming data from a connection. Several functions make it easy to + treat a CBUF as a kind of chunky, callback-driven stream.")) + +(define-condition cbuf-progress () + ((size + :initarg :size + :accessor cbuf-progress-size + :initform 0))) + +(defun call-processor (fun cbuf start end) + (signal 'cbuf-progress :size (- end start)) + (funcall fun (data cbuf) start end)) + +(defun make-cbuf (connection) + (make-instance 'cbuf :connection connection)) + +(defun make-stream-writer (stream) + "Create a callback for writing data to STREAM." + (lambda (data start end) + (write-sequence data stream :start start :end end))) + +(defgeneric size (cbuf) + (:method ((cbuf cbuf)) + (- (end cbuf) (start cbuf)))) + +(defgeneric emptyp (cbuf) + (:method ((cbuf cbuf)) + (zerop (size cbuf)))) + +(defgeneric refill (cbuf) + (:method ((cbuf cbuf)) + (when (eofp cbuf) + (error 'end-of-data)) + (setf (start cbuf) 0) + (setf (end cbuf) + (read-octets (data cbuf) + (connection cbuf))) + (cond ((emptyp cbuf) + (setf (eofp cbuf) t) + (error 'end-of-data)) + (t (size cbuf))))) + +(defun process-all (fun cbuf) + (unless (emptyp cbuf) + (call-processor fun cbuf (start cbuf) (end cbuf)))) + +(defun multi-cmatch (matchers cbuf) + (let (start end) + (dolist (matcher matchers (values start end)) + (multiple-value-bind (s e) + (match matcher (data cbuf) + :start (start cbuf) + :end (end cbuf)) + (when (and s (or (null start) (< s start))) + (setf start s + end e)))))) + +(defun cmatch (matcher cbuf) + (if (consp matcher) + (multi-cmatch matcher cbuf) + (match matcher (data cbuf) :start (start cbuf) :end (end cbuf)))) + +(defun call-until-end (fun cbuf) + (handler-case + (loop + (process-all fun cbuf) + (refill cbuf)) + (end-of-data () + (return-from call-until-end)))) + +(defun show-cbuf (context cbuf) + (format t "cbuf: ~A ~D - ~D~%" context (start cbuf) (end cbuf))) + +(defun call-for-n-octets (n fun cbuf) + (let ((remaining n)) + (loop + (when (<= remaining (size cbuf)) + (let ((end (+ (start cbuf) remaining))) + (call-processor fun cbuf (start cbuf) end) + (setf (start cbuf) end) + (return))) + (process-all fun cbuf) + (decf remaining (size cbuf)) + (refill cbuf)))) + +(defun call-until-matching (matcher fun cbuf) + (loop + (multiple-value-bind (start end) + (cmatch matcher cbuf) + (when start + (call-processor fun cbuf (start cbuf) end) + (setf (start cbuf) end) + (return))) + (process-all fun cbuf) + (refill cbuf))) + +(defun ignore-data (data start end) + (declare (ignore data start end))) + +(defun skip-until-matching (matcher cbuf) + (call-until-matching matcher 'ignore-data cbuf)) + + +;;; Creating HTTP requests as octet buffers + +(defclass octet-sink () + ((storage + :initarg :storage + :accessor storage)) + (:default-initargs + :storage (make-array 1024 :element-type 'octet + :fill-pointer 0 + :adjustable t)) + (:documentation "A simple stream-like target for collecting + octets.")) + +(defun add-octet (octet sink) + (vector-push-extend octet (storage sink))) + +(defun add-octets (octets sink &key (start 0) end) + (setf end (or end (length octets))) + (loop for i from start below end + do (add-octet (aref octets i) sink))) + +(defun add-string (string sink) + (loop for char across string + for code = (char-code char) + do (add-octet code sink))) + +(defun add-strings (sink &rest strings) + (mapc (lambda (string) (add-string string sink)) strings)) + +(defun add-newline (sink) + (add-octet 13 sink) + (add-octet 10 sink)) + +(defun sink-buffer (sink) + (subseq (storage sink) 0)) + +(defvar *proxy-url* nil) + +(defun full-proxy-path (host port path) + (format nil "~:[http~;https~]://~A~:[:~D~;~*~]~A" + (= port 443) + host + (or (= port 80) + (= port 443)) + port + path)) + +(defun make-request-buffer (host port path &key (method "GET")) + (setf method (string method)) + (when *proxy-url* + (setf path (full-proxy-path host port path))) + (let ((sink (make-instance 'octet-sink))) + (flet ((add-line (&rest strings) + (apply #'add-strings sink strings) + (add-newline sink))) + (add-line method " " path " HTTP/1.1") + (add-line "Host: " host (if (= port 80) "" + (format nil ":~D" port))) + (add-line "Connection: close") + ;; FIXME: get this version string from somewhere else. + (add-line "User-Agent: quicklisp-bootstrap/" + qlqs-info:*version*) + (add-newline sink) + (sink-buffer sink)))) + +(defun sink-until-matching (matcher cbuf) + (let ((sink (make-instance 'octet-sink))) + (call-until-matching + matcher + (lambda (buffer start end) + (add-octets buffer sink :start start :end end)) + cbuf) + (sink-buffer sink))) + + +;;; HTTP headers + +(defclass header () + ((data + :initarg :data + :accessor data) + (status + :initarg :status + :accessor status) + (name-starts + :initarg :name-starts + :accessor name-starts) + (name-ends + :initarg :name-ends + :accessor name-ends) + (value-starts + :initarg :value-starts + :accessor value-starts) + (value-ends + :initarg :value-ends + :accessor value-ends))) + +(defmethod print-object ((header header) stream) + (print-unreadable-object (header stream :type t) + (prin1 (status header) stream))) + +(defun matches-at (pattern target pos) + (= (mismatch pattern target :start2 pos) (length pattern))) + +(defun header-value-indexes (field-name header) + (loop with data = (data header) + with pattern = (ascii-vector (string-downcase field-name)) + for start across (name-starts header) + for i from 0 + when (matches-at pattern data start) + return (values (aref (value-starts header) i) + (aref (value-ends header) i)))) + +(defun ascii-header-value (field-name header) + (multiple-value-bind (start end) + (header-value-indexes field-name header) + (when start + (ascii-subseq (data header) start end)))) + +(defun all-field-names (header) + (map 'list + (lambda (start end) + (ascii-subseq (data header) start end)) + (name-starts header) + (name-ends header))) + +(defun headers-alist (header) + (mapcar (lambda (name) + (cons name (ascii-header-value name header))) + (all-field-names header))) + +(defmethod describe-object :after ((header header) stream) + (format stream "~&Decoded headers:~% ~S~%" (headers-alist header))) + +(defun content-length (header) + (let ((field-value (ascii-header-value "content-length" header))) + (when field-value + (let ((value (ignore-errors (parse-integer field-value)))) + (or value + (error "Content-Length header field value is not a number -- ~A" + field-value)))))) + +(defun chunkedp (header) + (string= (ascii-header-value "transfer-encoding" header) "chunked")) + +(defun location (header) + (ascii-header-value "location" header)) + +(defun status-code (vector) + (let* ((space (position (acode #\Space) vector)) + (c1 (- (aref vector (incf space)) 48)) + (c2 (- (aref vector (incf space)) 48)) + (c3 (- (aref vector (incf space)) 48))) + (+ (* c1 100) + (* c2 10) + (* c3 1)))) + +(defun force-downcase-field-names (header) + (loop with data = (data header) + for start across (name-starts header) + for end across (name-ends header) + do (loop for i from start below end + for code = (aref data i) + do (setf (aref data i) (ascii-downcase code))))) + +(defun skip-white-forward (pos vector) + (position-if-not 'whitep vector :start pos)) + +(defun skip-white-backward (pos vector) + (let ((nonwhite (position-if-not 'whitep vector :end pos :from-end t))) + (if nonwhite + (1+ nonwhite) + pos))) + +(defun contract-field-value-indexes (header) + "Header field values exclude leading and trailing whitespace; adjust +the indexes in the header accordingly." + (loop with starts = (value-starts header) + with ends = (value-ends header) + with data = (data header) + for i from 0 + for start across starts + for end across ends + do + (setf (aref starts i) (skip-white-forward start data)) + (setf (aref ends i) (skip-white-backward end data)))) + +(defun next-line-pos (vector) + (let ((pos 0)) + (labels ((finish (&optional (i pos)) + (return-from next-line-pos i)) + (after-cr (code) + (acase code + (:lf (finish pos)) + (t (finish (1- pos))))) + (pending (code) + (acase code + (:cr #'after-cr) + (:lf (finish pos)) + (t #'pending)))) + (let ((state #'pending)) + (loop + (setf state (funcall state (aref vector pos))) + (incf pos)))))) + +(defun make-hvector () + (make-array 16 :fill-pointer 0 :adjustable t)) + +(defun process-header (vector) + "Create a HEADER instance from the octet data in VECTOR." + (let* ((name-starts (make-hvector)) + (name-ends (make-hvector)) + (value-starts (make-hvector)) + (value-ends (make-hvector)) + (header (make-instance 'header + :data vector + :status 999 + :name-starts name-starts + :name-ends name-ends + :value-starts value-starts + :value-ends value-ends)) + (mark nil) + (pos (next-line-pos vector))) + (unless pos + (error "Unable to process HTTP header")) + (setf (status header) (status-code vector)) + (labels ((save (value vector) + (vector-push-extend value vector)) + (mark () + (setf mark pos)) + (clear-mark () + (setf mark nil)) + (finish () + (if mark + (save mark value-ends) + (save pos value-ends)) + (force-downcase-field-names header) + (contract-field-value-indexes header) + (return-from process-header header)) + (in-new-line (code) + (acase code + ((#\Tab #\Space) (setf mark nil) #'in-value) + (t + (when mark + (save mark value-ends)) + (clear-mark) + (save pos name-starts) + (in-name code)))) + (after-cr (code) + (acase code + (:lf #'in-new-line) + (t (in-new-line code)))) + (pending-value (code) + (acase code + ((#\Tab #\Space) #'pending-value) + (:cr #'after-cr) + (:lf #'in-new-line) + (t (save pos value-starts) #'in-value))) + (in-name (code) + (acase code + (#\: + (save pos name-ends) + (save (1+ pos) value-starts) + #'in-value) + ((:cr :lf) + (finish)) + ((#\Tab #\Space) + (error "Unexpected whitespace in header field name")) + (t + (unless (<= 0 code 127) + (error "Unexpected non-ASCII header field name")) + #'in-name))) + (in-value (code) + (acase code + (:lf (mark) #'in-new-line) + (:cr (mark) #'after-cr) + (t #'in-value)))) + (let ((state #'in-new-line)) + (loop + (incf pos) + (when (<= (length vector) pos) + (error "No header found in response")) + (setf state (funcall state (aref vector pos)))))))) + + +;;; HTTP URL parsing + +(defclass url () + ((hostname + :initarg :hostname + :accessor hostname + :initform nil) + (port + :initarg :port + :accessor port + :initform 80) + (path + :initarg :path + :accessor path + :initform "/"))) + +(defun parse-urlstring (urlstring) + (setf urlstring (string-trim " " urlstring)) + (let* ((pos (mismatch urlstring "http://" :test 'char-equal)) + (mark pos) + (url (make-instance 'url))) + (labels ((save () + (subseq urlstring mark pos)) + (mark () + (setf mark pos)) + (finish () + (return-from parse-urlstring url)) + (hostname-char-p (char) + (position char "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_." + :test 'char-equal)) + (at-start (char) + (case char + (#\/ + (setf (port url) nil) + (mark) + #'in-path) + (t + #'in-host))) + (in-host (char) + (case char + ((#\/ :end) + (setf (hostname url) (save)) + (mark) + #'in-path) + (#\: + (setf (hostname url) (save)) + (mark) + #'in-port) + (t + (unless (hostname-char-p char) + (error "~S is not a valid URL" urlstring)) + #'in-host))) + (in-port (char) + (case char + ((#\/ :end) + (setf (port url) + (parse-integer urlstring + :start (1+ mark) + :end pos)) + (mark) + #'in-path) + (t + (unless (digit-char-p char) + (error "Bad port in URL ~S" urlstring)) + #'in-port))) + (in-path (char) + (case char + ((#\# :end) + (setf (path url) (save)) + (finish))) + #'in-path)) + (let ((state #'at-start)) + (loop + (when (<= (length urlstring) pos) + (funcall state :end) + (finish)) + (setf state (funcall state (aref urlstring pos))) + (incf pos)))))) + +(defun url (thing) + (if (stringp thing) + (parse-urlstring thing) + thing)) + +(defgeneric request-buffer (method url) + (:method (method url) + (setf url (url url)) + (make-request-buffer (hostname url) (port url) (path url) + :method method))) + +(defun urlstring (url) + (format nil "~@[http://~A~]~@[:~D~]~A" + (hostname url) + (and (/= 80 (port url)) (port url)) + (path url))) + +(defmethod print-object ((url url) stream) + (print-unreadable-object (url stream :type t) + (prin1 (urlstring url) stream))) + +(defun merge-urls (url1 url2) + (setf url1 (url url1)) + (setf url2 (url url2)) + (make-instance 'url + :hostname (or (hostname url1) + (hostname url2)) + :port (or (port url1) + (port url2)) + :path (or (path url1) + (path url2)))) + + +;;; Requesting an URL and saving it to a file + +(defparameter *maximum-redirects* 10) +(defvar *default-url-defaults* (url "http://src.quicklisp.org/")) + +(defun read-http-header (cbuf) + (let ((header-data (sink-until-matching (list (acode-matcher :lf :lf) + (acode-matcher :cr :cr) + (acode-matcher :cr :lf :cr :lf)) + cbuf))) + (process-header header-data))) + +(defun read-chunk-header (cbuf) + (let* ((header-data (sink-until-matching (acode-matcher :cr :lf) cbuf)) + (end (or (position (acode :cr) header-data) + (position (acode #\;) header-data)))) + (values (parse-integer (ascii-subseq header-data 0 end) :radix 16)))) + +(defun save-chunk-response (stream cbuf) + "For a chunked response, read all chunks and write them to STREAM." + (let ((fun (make-stream-writer stream)) + (matcher (acode-matcher :cr :lf))) + (loop + (let ((chunk-size (read-chunk-header cbuf))) + (when (zerop chunk-size) + (return)) + (call-for-n-octets chunk-size fun cbuf) + (skip-until-matching matcher cbuf))))) + +(defun save-response (file header cbuf) + (with-open-file (stream file + :direction :output + :if-exists :supersede + :element-type 'octet) + (let ((content-length (content-length header))) + (cond ((chunkedp header) + (save-chunk-response stream cbuf)) + (content-length + (call-for-n-octets content-length + (make-stream-writer stream) + cbuf)) + (t + (call-until-end (make-stream-writer stream) cbuf)))))) + +(defun call-with-progress-bar (size fun) + (let ((progress-bar (make-progress-bar size))) + (start-display progress-bar) + (flet ((update (condition) + (update-progress progress-bar + (cbuf-progress-size condition)))) + (handler-bind ((cbuf-progress #'update)) + (funcall fun))) + (finish-display progress-bar))) + +(defun fetch (url file &key (follow-redirects t) quietly + (maximum-redirects *maximum-redirects*)) + "Request URL and write the body of the response to FILE." + (setf url (merge-urls url *default-url-defaults*)) + (setf file (merge-pathnames file)) + (let ((redirect-count 0) + (original-url url) + (connect-url (or (url *proxy-url*) url)) + (stream (if quietly + (make-broadcast-stream) + *trace-output*))) + (loop + (when (<= maximum-redirects redirect-count) + (error "Too many redirects for ~A" original-url)) + (with-connection (connection (hostname connect-url) (port connect-url)) + (let ((cbuf (make-instance 'cbuf :connection connection)) + (request (request-buffer "GET" url))) + (write-octets request connection) + (let ((header (read-http-header cbuf))) + (loop while (= (status header) 100) + do (setf header (read-http-header cbuf))) + (cond ((= (status header) 200) + (let ((size (content-length header))) + (format stream "~&; Fetching ~A~%" url) + (if (and (numberp size) + (plusp size)) + (format stream "; ~$KB~%" (/ size 1024)) + (format stream "; Unknown size~%")) + (if quietly + (save-response file header cbuf) + (call-with-progress-bar (content-length header) + (lambda () + (save-response file header cbuf)))))) + ((not (<= 300 (status header) 399)) + (error "Unexpected status for ~A: ~A" + url (status header)))) + (if (and follow-redirects (<= 300 (status header) 399)) + (let ((new-urlstring (ascii-header-value "location" header))) + (when (not new-urlstring) + (error "Redirect code ~D received, but no Location: header" + (status header))) + (incf redirect-count) + (setf url (merge-urls new-urlstring + url)) + (format stream "~&; Redirecting to ~A~%" url)) + (return (values header (and file (probe-file file))))))))))) + + +;;; A primitive tar unpacker + +(in-package #:qlqs-minitar) + +(defun make-block-buffer () + (make-array 512 :element-type '(unsigned-byte 8) :initial-element 0)) + +(defun skip-n-blocks (n stream) + (let ((block (make-block-buffer))) + (dotimes (i n) + (read-sequence block stream)))) + +(defun ascii-subseq (vector start end) + (let ((string (make-string (- end start)))) + (loop for i from 0 + for j from start below end + do (setf (char string i) (code-char (aref vector j)))) + string)) + +(defun block-asciiz-string (block start length) + (let* ((end (+ start length)) + (eos (or (position 0 block :start start :end end) + end))) + (ascii-subseq block start eos))) + +(defun prefix (header) + (when (plusp (aref header 345)) + (block-asciiz-string header 345 155))) + +(defun name (header) + (block-asciiz-string header 0 100)) + +(defun payload-size (header) + (values (parse-integer (block-asciiz-string header 124 12) :radix 8))) + +(defun nth-block (n file) + (with-open-file (stream file :element-type '(unsigned-byte 8)) + (let ((block (make-block-buffer))) + (skip-n-blocks (1- n) stream) + (read-sequence block stream) + block))) + +(defun payload-type (code) + (case code + (0 :file) + (48 :file) + (53 :directory) + (t :unsupported))) + +(defun full-path (header) + (let ((prefix (prefix header)) + (name (name header))) + (if prefix + (format nil "~A/~A" prefix name) + name))) + +(defun save-file (file size stream) + (multiple-value-bind (full-blocks partial) + (truncate size 512) + (ensure-directories-exist file) + (with-open-file (outstream file + :direction :output + :if-exists :supersede + :element-type '(unsigned-byte 8)) + (let ((block (make-block-buffer))) + (dotimes (i full-blocks) + (read-sequence block stream) + (write-sequence block outstream)) + (when (plusp partial) + (read-sequence block stream) + (write-sequence block outstream :end partial)))))) + +(defun unpack-tarball (tarfile &key (directory *default-pathname-defaults*)) + (let ((block (make-block-buffer))) + (with-open-file (stream tarfile :element-type '(unsigned-byte 8)) + (loop + (let ((size (read-sequence block stream))) + (when (zerop size) + (return)) + (unless (= size 512) + (error "Bad size on tarfile")) + (when (every #'zerop block) + (return)) + (let* ((payload-code (aref block 156)) + (payload-type (payload-type payload-code)) + (tar-path (full-path block)) + (full-path (merge-pathnames tar-path directory)) + (payload-size (payload-size block))) + (case payload-type + (:file + (save-file full-path payload-size stream)) + (:directory + (ensure-directories-exist full-path)) + (t + (warn "Unknown tar block payload code -- ~D" payload-code) + (skip-n-blocks (ceiling (payload-size block) 512) stream))))))))) + +(defun contents (tarfile) + (let ((block (make-block-buffer)) + (result '())) + (with-open-file (stream tarfile :element-type '(unsigned-byte 8)) + (loop + (let ((size (read-sequence block stream))) + (when (zerop size) + (return (nreverse result))) + (unless (= size 512) + (error "Bad size on tarfile")) + (when (every #'zerop block) + (return (nreverse result))) + (let* ((payload-type (payload-type (aref block 156))) + (tar-path (full-path block)) + (payload-size (payload-size block))) + (skip-n-blocks (ceiling payload-size 512) stream) + (case payload-type + (:file + (push tar-path result)) + (:directory + (push tar-path result))))))))) + + +;;; +;;; The actual bootstrapping work +;;; + +(in-package #:quicklisp-quickstart) + +(defvar *home* + (merge-pathnames (make-pathname :directory '(:relative "quicklisp")) + (user-homedir-pathname))) + +(defun qmerge (pathname) + (merge-pathnames pathname *home*)) + +(defun renaming-fetch (url file) + (let ((tmpfile (qmerge "tmp/fetch.dat"))) + (fetch url tmpfile) + (rename-file tmpfile file))) + +(defvar *quickstart-parameters* nil + "This plist is populated with parameters that may carry over to the + initial configuration of the client, e.g. :proxy-url + or :initial-dist-url") + +(defvar *quicklisp-hostname* "beta.quicklisp.org") + +(defvar *client-info-url* + (format nil "http://~A/client/quicklisp.sexp" + *quicklisp-hostname*)) + +(defclass client-info () + ((setup-url + :reader setup-url + :initarg :setup-url) + (asdf-url + :reader asdf-url + :initarg :asdf-url) + (client-tar-url + :reader client-tar-url + :initarg :client-tar-url) + (version + :reader version + :initarg :version) + (plist + :reader plist + :initarg :plist) + (source-file + :reader source-file + :initarg :source-file))) + +(defmethod print-object ((client-info client-info) stream) + (print-unreadable-object (client-info stream :type t) + (prin1 (version client-info) stream))) + +(defun safely-read (stream) + (let ((*read-eval* nil)) + (read stream))) + +(defun fetch-client-info-plist (url) + "Fetch and return the client info data at URL." + (let ((local-client-info-file (qmerge "tmp/client-info.sexp"))) + (ensure-directories-exist local-client-info-file) + (renaming-fetch url local-client-info-file) + (with-open-file (stream local-client-info-file) + (list* :source-file local-client-info-file + (safely-read stream))))) + +(defun fetch-client-info (url) + (let ((plist (fetch-client-info-plist url))) + (destructuring-bind (&key setup asdf client-tar version + source-file + &allow-other-keys) + plist + (unless (and setup asdf client-tar version) + (error "Invalid data from client info URL -- ~A" url)) + (make-instance 'client-info + :setup-url (getf setup :url) + :asdf-url (getf asdf :url) + :client-tar-url (getf client-tar :url) + :version version + :plist plist + :source-file source-file)))) + +(defun client-info-url-from-version (version) + (format nil "http://~A/client/~A/client-info.sexp" + *quicklisp-hostname* + version)) + +(defun distinfo-url-from-version (version) + (format nil "http://~A/dist/~A/distinfo.txt" + *quicklisp-hostname* + version)) + +(defvar *help-message* + (format nil "~&~% ==== quicklisp quickstart install help ====~%~% ~ + quicklisp-quickstart:install can take the following ~ + optional arguments:~%~% ~ + :path \"/path/to/installation/\"~%~% ~ + :proxy \"http://your.proxy:port/\"~%~% ~ + :client-url ~%~% ~ + :client-version ~%~% ~ + :dist-url ~%~% ~ + :dist-version ~%~%")) + +(defvar *after-load-message* + (format nil "~&~% ==== quicklisp quickstart ~A loaded ====~%~% ~ + To continue with installation, evaluate: (quicklisp-quickstart:install)~%~% ~ + For installation options, evaluate: (quicklisp-quickstart:help)~%~%" + qlqs-info:*version*)) + +(defvar *after-initial-setup-message* + (with-output-to-string (*standard-output*) + (format t "~&~% ==== quicklisp installed ====~%~%") + (format t " To load a system, use: (ql:quickload \"system-name\")~%~%") + (format t " To find systems, use: (ql:system-apropos \"term\")~%~%") + (format t " To load Quicklisp every time you start Lisp, use: (ql:add-to-init-file)~%~%") + (format t " For more information, see http://www.quicklisp.org/beta/~%~%"))) + +(defun initial-install (&key (client-url *client-info-url*) dist-url) + (setf *quickstart-parameters* + (list :proxy-url *proxy-url* + :initial-dist-url dist-url)) + (ensure-directories-exist (qmerge "tmp/")) + (let ((client-info (fetch-client-info client-url)) + (tmptar (qmerge "tmp/quicklisp.tar")) + (setup (qmerge "setup.lisp")) + (asdf (qmerge "asdf.lisp"))) + (renaming-fetch (client-tar-url client-info) tmptar) + (unpack-tarball tmptar :directory (qmerge "./")) + (renaming-fetch (setup-url client-info) setup) + (renaming-fetch (asdf-url client-info) asdf) + (rename-file (source-file client-info) (qmerge "client-info.sexp")) + (load setup :verbose nil :print nil) + (write-string *after-initial-setup-message*) + (finish-output))) + +(defun help () + (write-string *help-message*) + t) + +(defun non-empty-file-namestring (pathname) + (let ((string (file-namestring pathname))) + (unless (or (null string) + (equal string "")) + string))) + +(defun install (&key ((:path *home*) *home*) + ((:proxy *proxy-url*) *proxy-url*) + client-url + client-version + dist-url + dist-version) + (setf *home* (merge-pathnames *home* (truename *default-pathname-defaults*))) + (let ((name (non-empty-file-namestring *home*))) + (when name + (warn "Making ~A part of the install pathname directory" + name) + ;; This corrects a pathname like "/foo/bar" to "/foo/bar/" and + ;; "foo" to "foo/" + (setf *home* + (make-pathname :defaults *home* + :directory (append (pathname-directory *home*) + (list name)))))) + (let ((setup-file (qmerge "setup.lisp"))) + (when (probe-file setup-file) + (multiple-value-bind (result proceed) + (with-simple-restart (load-setup "Load ~S" setup-file) + (error "Quicklisp has already been installed. Load ~S instead." + setup-file)) + (declare (ignore result)) + (when proceed + (return-from install (load setup-file)))))) + (if (find-package '#:ql) + (progn + (write-line "!!! Quicklisp has already been set up. !!!") + (write-string *after-initial-setup-message*) + t) + (call-with-quiet-compilation + (lambda () + (let ((client-url (or client-url + (and client-version + (client-info-url-from-version client-version)) + *client-info-url*)) + ;; It's ok for dist-url to be nil; there's a default in + ;; the client + (dist-url (or dist-url + (and dist-version + (distinfo-url-from-version dist-version))))) + (initial-install :client-url client-url + :dist-url dist-url)))))) + +(write-string *after-load-message*) + +;;; End of quicklisp.lisp diff --git a/test.lisp b/test.lisp new file mode 100644 index 0000000..836442f --- /dev/null +++ b/test.lisp @@ -0,0 +1,26 @@ +(let ((equipment '(armor (head + (dragon-helmet) + legs (empty) + arms (empty)) + weapon (left + (wooden-shield) + right (empty)) + ring (left + (empty) + right (empty))))) + (defun get-armor() + (getf equipment 'armor)) + (defun get-helmet() + (getf (get-armor) 'head)) + (defun get-legs() + (getf (get-armor) 'legs)) + (defun get-arms() + (getf(get-armor) 'legs)) + + (defun change-helmet (helmet) + "Useage: (change-helmet 'god-helmet" + (setf (car (get-helmet)) helmet)) + (defun change-legs (legs) + (setf (car (get-legs)) legs)) + (defun change-arms (arms) + (setf (car (get-arms)) arms))) diff --git a/tktest/README b/tktest/README new file mode 100644 index 0000000..5d6df25 --- /dev/null +++ b/tktest/README @@ -0,0 +1,5 @@ +Make sure TK is installed. In mint I was able to: + +tux i wish + +Other distros may require TK since apt switched to package TK from WISH diff --git a/tktest/build-script.sh b/tktest/build-script.sh new file mode 100755 index 0000000..c325531 --- /dev/null +++ b/tktest/build-script.sh @@ -0,0 +1,7 @@ +#!/bin/sh +sbcl --eval "(progn + (compile-file \"ltk\") + (load \"ltk\") + (compile-file \"hello-world\") + (load \"hello-world\") + (save-lisp-and-die \"hello-world.core\"))" diff --git a/tktest/example.lisp b/tktest/example.lisp new file mode 100644 index 0000000..3178e8e --- /dev/null +++ b/tktest/example.lisp @@ -0,0 +1,16 @@ +(defpackage :hello-world + (:use :common-lisp 'ltk) + (:export #:main)) + +(in-package :hello-world) + +(defun main () + (setf *debug-tk* nil) + (with-ltk () + (let ((b (make-instance + `button + :text "Hello World!" + :command (lambda () + (do-msg "Bye!") + (setf *exit-mainloop* t))))) + (pack b)))) diff --git a/tktest/hello-world.core b/tktest/hello-world.core new file mode 100644 index 0000000..5bd55ed Binary files /dev/null and b/tktest/hello-world.core differ diff --git a/tktest/hello-world.fasl b/tktest/hello-world.fasl new file mode 100644 index 0000000..4e15b91 Binary files /dev/null and b/tktest/hello-world.fasl differ diff --git a/tktest/hello-world.lisp b/tktest/hello-world.lisp new file mode 100644 index 0000000..a8fd41b --- /dev/null +++ b/tktest/hello-world.lisp @@ -0,0 +1,16 @@ +(defpackage :hello-world + (:use :common-lisp ltk) + (:export #:main)) + +(in-package :hello-world) + +(defun main () + (setf *debug-tk* nil) + (with-ltk () + (let ((b (make-instance + `button + :text "Hello World!" + :command (lambda () + (do-msg "Bye!") + (setf *exit-mainloop* t))))) + (pack b)))) diff --git a/tktest/ltk.fasl b/tktest/ltk.fasl new file mode 100644 index 0000000..32677b6 Binary files /dev/null and b/tktest/ltk.fasl differ diff --git a/tktest/ltk.lisp b/tktest/ltk.lisp new file mode 100644 index 0000000..d47c26b --- /dev/null +++ b/tktest/ltk.lisp @@ -0,0 +1,5342 @@ +#| + + This software is Copyright (c) 2003-2010 Peter Herth + Portions Copyright (c) 2005-2010 Thomas F. Burdick + Portions Copyright (c) 2006-2010 Cadence Design Systems + Portions Copyright (c) 2010 Daniel Herring + + The authors grant you the rights to distribute + and use this software as governed by the terms + of the Lisp Lesser GNU Public License + (http://opensource.franz.com/preamble.html), + known as the LLGPL. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + +|# + +#| +All tk commands as of version 8.4 with support information. "-" means not +supported by purpose (look comment), "x" means supported, though some +options may not be supported. + +command supported comment +bell x +bind x +bindtags modify the tag list of a widget that describes which events it gets +bitmap - see image +button x +canvas x +checkbutton x +clipboard x (canvas get missing... tricky...) +colors - constants only +console - only on some platforms +cursors x +destroy x +entry x +event create and manage virtual events +focus x focus management functions +font +frame x +grab +grid x +image x +keysyms - constants only +label x +labelframe x +listbox x +loadTk - +lower x +menu x +menubutton x +message x +option - +options - only helpfile +pack x +panedwindow x +photo x +place x geometry manager using coordinates +radiobutton x +raise x +scale x +scrollbar x +selection +send +spinbox x +text x +tk +tk_bisque - only for tk backwards compatibility +tk_chooseColor +tk_chooseDirectory +tk_dialog +tk_focusFollowsMouse +tk_focusNext +tk_focusPrev +tk_getOpenFile x +tk_getSaveFile x +tk_menuSetFocus - +tk_messageBox x +tk_optionMenu +tk_popup +tk_setPalette - +tk_textCopy +tk_textCut +tk_textPaste +tkerror - +tkvars - +tkwait +toplevel x +winfo x +wm x + + +support of all config args as keywords to make-instance: + +bitmap +button x +canvas x +checkbutton x +entry x +frame x +image +label x +labelframe x +listbox x +menu +menubutton +message +panedwindow x +photo +radiobutton x +scale x +scrollbar x +spinbox x +text x +toplevel x + +|# + + +(defpackage :ltk + (:use :common-lisp + #+(or :cmu :scl) :ext + #+:sbcl :sb-ext + ) + (:shadow #+:sbcl #:exit + #+:sbcl #:create) + (:export #:ltktest + #:*ltk-version* + #:*cursors* + #:*debug-tk* + #:*debug-buffers* + #:*break-mainloop* + #:*exit-mainloop* + #:*init-wish-hook* + #:*mb-icons* + #:*ltk-debug* + #:*tk* + #:*wish* + #:wish-stream + #:wish-variable + #:wish-variables + #:*wish-args* + #:*wish-pathname* + #:*default-ltk-debugger* + #:add-pane + #:add-separator + #:after + #:after-cancel + #:after-idle + #:append-text + #:append-newline + #:ask-okcancel + #:ask-yesno + #:background + #:bbox + #:bell + #:bind + #:button + #:calc-scroll-region + #:canvas + #:canvas-line + #:canvas-oval + #:canvas-polygon + #:canvas-rectangle + #:canvas-text + #:canvas-image + #:canvas-item + #:canvas-arc + #:canvas-bbox + #:canvas-window + #:canvasx + #:canvasy + #:cget + #:check-button + #:choose-color + #:choose-directory + #:clear-text + #:clear + #:clipboard-append + #:clipboard-clear + #:clipboard-get + #-:tk84 + #:combobox + #:command + #:coords + #:configure + #:create-arc + #:create-bitmap + #:create-image + #:create-line + #:create-line* + #:create-menu2 + #:create-oval + #:create-polygon + #:create-rectangle + #:create-text + #:create-window + #:*debug-settings-table* + #:defargs + #:deiconify + #:destroy + #:do-execute + #:do-msg + #:entry + #:entry-select + #:exit-wish + #:event + #:event-x + #:event-y + #:event-keycode + #:event-char + #:event-mouse-button + #:event-root-x + #:event-root-y + #:event-width + #:event-height + #:focus + #:font-configure + #:font-create + #:font-delete + #:font-metrics + #:force-focus + #:forget-pane + #:format-wish + #:frame + #:geometry + #:get-open-file + #:get-save-file + #:grab + #:grab-release + #:grid + #:grid-columnconfigure + #:grid-configure + #:grid-forget + #:grid-rowconfigure + #:hscroll + #:iconify + #:iconwindow + #:image-load + #:image-setpixel + #:cursor-index + #:input-box + #:insert-object + #:interior + #:itembind + #:itemconfigure + #:itemdelete + #:itemmove + #:itemlower + #:itemraise + #:label + #:labelframe + #:listbox + #:listbox-append + #:listbox-clear + #:listbox-delete + #:listbox-configure + #:listbox-get-selection + #:listbox-nearest + #:listbox-select + #:load-text + #:lower + #:mainloop + #:make-items + #:create-items + #:make-canvas + #:make-frame + #:make-image + #:make-label + #:make-menu + #:make-menubar + #:make-menubutton + #:make-scrollbar + #:make-scrolled-canvas + #:make-text + #:make-toplevel + #:make-line + #:make-oval + #:make-polygon + #:make-rectangle + #:master + #:maxsize + #:menu + #:menubar + #:menubutton + #:menucheckbutton + #:menu-delete + #:menuradiobutton + #:message + #:message-box + #:minsize + #:move + #:move-all + #:normalize + #-:tk84 + #:notebook + #:on-close + #:on-focus + #:pack + #:pack-forget + #:pack-forget-all + #:pack-propagate + #:paned-window + #:photo-image + #:*ping-interval-seconds* + #:place + #:place-forget + #:popup + #:postscript + #:process-events + #:radio-button + #:raise + #:read-event + #:resizable + #:sash-coord + #:sash-place + #:save-text + #:scale + #:screen-height + #:screen-height-mm + #:screen-mouse + #:screen-mouse-x + #:screen-mouse-y + #:screen-width + #:screen-width-mm + #:scrollbar + #:scrolled-canvas + #:scrolled-frame + #:scrolled-listbox + #:scrolled-text + #:scrollregion + #:search-all-text + #:search-next-text + #:see + #:send-lazy + #:send-wish + #:set-coords + #:set-coords* + #:set-focus-next + #:set-geometry + #:set-geometry-wh + #:set-geometry-xy + #:set-wm-overrideredirect + #:spinbox + #:start-wish + #:tag-bind + #:tag-configure + #:text + #:textbox + #:tkobject + #:title + #:toplevel + #:value + #:options + #:vscroll + #:widget + #:widget-path + #:window-height + #:window-id + #:window-width + #:window-x + #:window-y + #:window-transient + #:make-ltk-connection + #:widget-class-name + #:with-atomic + #:with-ltk + #:call-with-ltk + #:exit-with-remote-ltk + #:with-modal-toplevel + #:with-remote-ltk + #:with-widgets + #:withdraw + #-:tk84 + #:wm-forget + #-:tk84 + #:wm-manage + #:wm-title + #:wm-state + #:with-hourglass + #:notebook-index + #:notebook-add + #:notebook-tab + #:notebook-forget + #:notebook-hide + #:notebook-identify + #:notebook-select + #:notebook-events + #:notebook-enable-traversal + #:defwidget + #:progressbar + #:length + #:mode + #:maximum + #:phase + #:separator + #:sizegrip + #:treeview + #:treeview-delete + #:column-configure + #:children + #:treeview-focus + #:treeview-exists + #:dictionary-plist + #:treeview-insert + #:treeview-item + #:treeview-column + #:treeview-heading + #:treeview-move + #:treeitem + #:self + #:reset-scroll + #:scroll-to-top + #:tagbind + #:pane-configure + #:handle + #:column-values + #:listbox-insert + #:font-families + #:scrolled-treeview + #:treeview-get-selection + #:treeview-identify + #:treeview-identify-item + #:treeview-set-selection + #:items + #:image)) + +(defpackage :ltk-user + (:use :common-lisp :ltk)) + +(in-package :ltk) +;communication with wish +;;; this ist the only function to adapted to other lisps + +(eval-when (:compile-toplevel :load-toplevel :execute) + ;;(pushnew :tk85 *features*) + ;;(pushnew :tktable *features*) + ) + +(defun do-execute (program args &optional (wt nil)) + "execute program with args a list containing the arguments passed to the program + if wt is non-nil, the function will wait for the execution of the program to return. + returns a two way stream connected to stdin/stdout of the program" + #+(or :clisp :lispworks) (declare (ignore wt)) + (let ((fullstring program)) + (dolist (a args) + (setf fullstring (concatenate 'string fullstring " " a))) + #+(or :cmu :scl) + (let ((proc (run-program program args :input :stream :output :stream :wait wt + #+scl :external-format #+scl :utf-8))) + (unless proc + (error "Cannot create process.")) + (make-two-way-stream + (ext:process-output proc) + (ext:process-input proc)) + ) + #+:clisp (let ((proc (ext:run-program program :arguments args :input :stream :output :stream :wait t))) + (unless proc + (error "Cannot create process.")) + proc + ) + #+:sbcl (let ((proc (sb-ext:run-program program args :input :stream :output :stream :wait wt :search t))) + (unless proc + (error "Cannot create process.")) + #+:ext-8859-1 + (make-two-way-stream + (sb-sys:make-fd-stream + (sb-sys:fd-stream-fd (process-output proc)) + :input t :external-format :iso-8859-1) + (sb-sys:make-fd-stream + (sb-sys:fd-stream-fd (process-input proc)) + :output t :external-format :iso-8859-1)) + + (make-two-way-stream + (sb-sys:make-fd-stream + (sb-sys:fd-stream-fd (process-output proc)) + :input t :external-format :utf-8) + (sb-sys:make-fd-stream + (sb-sys:fd-stream-fd (process-input proc)) + :output t :external-format :utf-8)) + #+:xxext-8859-1 + (make-two-way-stream + (process-output proc) + (process-input proc)) + ) + #+:lispworks (system:open-pipe fullstring :direction :io) + #+:allegro (let ((proc (excl:run-shell-command + #+:mswindows fullstring + #-:mswindows (apply #'vector program program args) + :input :stream :output :stream :wait wt))) + (unless proc + (error "Cannot create process.")) + proc + ) + #+:ecl(ext:run-program program args :input :stream :output :stream +:error :output :wait wt) + #+:openmcl (let ((proc (ccl:run-program program args :input + :stream :output :stream :wait wt))) + (unless proc + (error "Cannot create process.")) + (make-two-way-stream + (ccl:external-process-output-stream proc) + (ccl:external-process-input-stream proc))) + )) + +(defvar *ltk-version* "0.992") + +;;; global var for holding the communication stream +(defstruct (ltk-connection (:constructor make-ltk-connection (&key remotep)) + (:conc-name #:wish-)) + (stream nil) + (callbacks (make-hash-table :test #'equal)) + (after-ids (make-hash-table :test #'equal)) + (counter 1) + (after-counter 1) + (event-queue nil) + ;; This is should be a function that takes a thunk, and calls it in + ;; an environment with some condition handling in place. It is what + ;; allows the user to specify error-handling in START-WISH, and have + ;; it take place inside of MAINLOOP. + (call-with-condition-handlers-function (lambda (f) (funcall f))) + ;; This is only used to support SERVE-EVENT. + (input-handler nil) + (remotep nil) + (output-buffer nil) + (variables (make-hash-table :test #'equal)) + ) + +(defmethod wish-variable (name (wish ltk-connection)) + (gethash name (wish-variables wish))) + +(defmethod (setf wish-variable) (val name (wish ltk-connection)) + (setf (gethash name (wish-variables wish)) val)) + + +(defmacro with-ltk-handlers (() &body body) + `(funcall (wish-call-with-condition-handlers-function *wish*) + (lambda () ,@body))) + +;;; global connection information + +(eval-when (:compile-toplevel :load-toplevel :execute) + (setf + (documentation 'make-ltk-connection 'function) + "Create a new LTK-CONNECTION object. This represents a connection to a + specific wish. You can maintain connections to several distinct wish + processes by binding *WISH* to the one you desire to communicate with, and + using LTK functions within that dynamic scope.")) + +(define-condition ltk-error (simple-error) ()) +(defun ltk-error (format &rest args) + (error 'ltk-error :format-control format :format-arguments args)) + +(defvar *wish* (make-ltk-connection) + "The current connection to an inferior wish.") + +(defvar *wish-connections* () + "Connections pushed aside by invoking the NEW-WISH restart in START-WISH.") + +;;; verbosity of debug messages, if true, then all communication +;;; with tk is echoed to stdout +(defvar *debug-tk* nil) + +;; if set to t, ltk will report the buffer size sent to tk +(defvar *debug-buffers* nil) + +(defvar *trace-tk* nil) + +(defvar *wish-pathname* + #+freebsd "wish8.5" + #-freebsd "wish") + +(defvar *wish-args* '("-name" "LTK")) + +(defvar *init-wish-hook* nil) + +(defparameter *buffer-for-atomic-output* nil) + +(defun dbg (fmt &rest args) +; (with-open-file (w "rl.log" :direction :output :if-exists :append :if-does-not-exist :create) +; (apply #'format w fmt args) +; (finish-output w)) + (when *debug-tk* + (apply #'format *trace-output* fmt args) + (finish-output *trace-output*) + )) + +(defmacro with-atomic (&rest code) + `(let ((*buffer-for-atomic-output* t)) + ,@code + (flush-wish))) + +(defmacro send-lazy (&rest code) + `(let ((*buffer-for-atomic-output* t)) + ,@code + )) + + +;;; setup of wish +;;; put any tcl function definitions needed for running ltk here +(defun init-wish () + (send-lazy + ;; print string readable, escaping all " and \ + ;; proc esc {s} {puts "\"[regsub {"} [regsub {\\} $s {\\\\}] {\"}]\""} + ;(send-wish "proc esc {s} {puts \"\\\"[regsub -all {\"} [regsub -all {\\\\} $s {\\\\\\\\}] {\\\"}]\\\"\"} ") + ;(send-wish "proc escape {s} {return [regsub -all {\"} [regsub -all {\\\\} $s {\\\\\\\\}] {\\\"}]} ") + (send-wish "package require Tk") + (flush-wish) + + #+:tk84 + (send-wish "catch {package require Ttk}") + #-:tk84 + (send-wish "if {[catch {package require Ttk} err]} {tk_messageBox -icon error -type ok -message \"$err\"}") + + + (send-wish "proc debug { msg } { + global server + puts $server \"(:debug \\\"[escape $msg]\\\")\" + flush $server + } ") + + (send-wish "proc escape {s} {regsub -all {\\\\} $s {\\\\\\\\} s1;regsub -all {\"} $s1 {\\\"} s2;return $s2}") + ;;; proc senddata {s} {puts "(data \"[regsub {"} [regsub {\\} $s {\\\\}] {\"}]\")"} + (send-wish "proc senddata {s} {global server; puts $server \"(:data [escape $s])\";flush $server}") + + (send-wish "proc senddatastring {s} { + global server + + puts $server \"(:data \\\"[escape $s]\\\")\" + flush $server + } ") + + (send-wish "proc senddatastrings {strings} { + global server + puts $server \"(:data (\" + foreach s $strings { + puts $server \"\\\"[escape $s]\\\"\" + } + puts $server \"))\";flush $server} ") + (send-wish "proc to_keyword {s} { + if {[string index $s 0] == \"-\"} { + return \":[string range $s 1 [string length $s]]\" } {return \":$s\"}}") + + (send-wish "proc sendpropertylist {l} { + global server; + set pos 0 + set ll [llength $l] + puts $server \"(:data (\" + while {$pos < $ll} { + puts $server \" [to_keyword [lindex $l $pos]] \" + set pos [expr $pos + 1] + puts $server \" [lindex $l $pos] \" + set pos [expr $pos + 1] + } + puts $server \"))\" + +}") + + (send-wish "proc searchall {widget pattern} { + set l [string length $pattern] + set result [$widget search $pattern 1.0] + set previous 0 + while {$result > $previous} { + $widget tag add sel $result $result+${l}chars + set previous $result + set result [$widget search $pattern $result+${l}chars] + } + }") + + (send-wish "proc searchnext {widget pattern} { + set l [string length $pattern] + set result [$widget search $pattern insert] + if {$result > 0} { + $widget tag remove sel 1.0 end + $widget tag add sel $result $result+${l}chars + $widget mark set insert $result+${l}chars + $widget see insert + } + }") + + (send-wish "proc resetScroll {c} { + $c configure -scrollregion [$c bbox all] +} + +proc moveToStart {sb} { + set range [$sb get] + $sb set 0 [expr [lindex $range 1] - [lindex $range 0]] + } +") + + ;;; proc sendevent {s} {puts "(event \"[regsub {"} [regsub {\\} $s {\\\\}] {\"}]\")"} + ;(send-wish "proc sendevent {s x y keycode char width height root_x root_y} {puts \"(:event \\\"$s\\\" $x $y $keycode $char $width $height $root_x $root_y)\"} ") + (send-wish "proc sendevent {s x y keycode char width height root_x root_y mouse_button} {global server; puts $server \"(:event \\\"$s\\\" $x $y $keycode $char $width $height $root_x $root_y $mouse_button)\"; flush $server} ") + ;;; proc callback {s} {puts "(callback \"[regsub {"} [regsub {\\} $s {\\\\}] {\"}]\")"} + + ;;; callback structure: (:callback "widgetname") ;; for non-parameter callbacks + ;;; (:callback "widgetname" val) ;; wideget returns non-string value + ;;; (:callback "widgetname" "string") ;; widget returns string value + + (send-wish "proc callback {s} {global server; puts $server \"(:callback \\\"$s\\\")\";flush $server} ") + (send-wish "proc callbackval {s val} {global server; puts $server \"(:callback \\\"$s\\\" $val)\"} ") + (send-wish "proc callbackstring {s val} {global server; puts $server \"(:callback \\\"$s\\\" \\\"[escape $val]\\\")\"} ") + (send-wish "proc keepalive {} {global server; puts $server \"(:keepalive \\\"[clock format [clock seconds] -format \"%d/%m/%y %T\"]\\\")\"; flush $server}") + ;(send-wish "global serverlist;set serverlist {{foo 10} {bar 20} {baz 40}}") + ;(send-wish "global host; set host bar") + ;(send-wish "global hping; set hping 42") + + (dolist (fun *init-wish-hook*) ; run init hook funktions + (funcall fun)))) + + +(defun init-tcl (&key debug-tcl) + (let ((translation "lf")) + #+(and (or windows win32) (not sbcl)) (setf translation "crlf") + + (format (wish-stream *wish*) " +set buffer {} +set server stdout + +set tclside_ltkdebug ~:[0~;1~] +package require Tk +wm protocol . WM_DELETE_WINDOW exit + +if {$tclside_ltkdebug} { + toplevel .ltk + wm title .ltk \"Debug output\" + text .ltk.debug -height 20 + pack .ltk.debug -side left -expand 1 -fill both + scrollbar .ltk.vs -orient vertical -command {.ltk.debug yview} + .ltk.debug configure -yscrollcommand {.ltk.vs set} + pack .ltk.vs -side right -fill y +} + +proc ltkdebug {text} { + global tclside_ltkdebug + if {$tclside_ltkdebug} { + .ltk.debug insert end \"$text\\\n\" + .ltk.debug see end + } +} + + +proc getcount {s} { + if {[regexp {^\\s*(\\d+) } $s match num]} { + return $num + } +} + +proc getstring {s} { + if {[regexp {^\\s*(\\d+) } $s match]} { + return [string range $s [string length $match] end] + } +} + +proc escape_for_lisp {s} {regsub -all {\\\\} $s {\\\\\\\\} s1;regsub -all {\"} $s1 {\\\"} s2;return $s2} + +proc process_buffer {} { + global buffer + global server + set cmd $buffer + set buffer {} + if {[catch $cmd result]>0} { + # tk_messageBox -icon error -type ok -title \"Error!\" -message $result + puts $server \"(:error \\\"[escape_for_lisp $result]\\\")\" + flush $server + } +} + +proc bt {txt} { + global buffer + append buffer $txt +} + +fconfigure stdin -encoding utf-8 -translation ~a +fconfigure stdout -encoding utf-8 +#fileevent stdin readable sread +" debug-tcl translation))) + +;;; start wish and set (wish-stream *wish*) +(defun start-wish (&rest keys &key debugger-class remotep stream debug-tcl) + ;; open subprocess + (if (null (wish-stream *wish*)) + (progn + (setf (wish-stream *wish*) (or stream (do-execute *wish-pathname* *wish-args*))) + #+(or mswindows windows win32) (sleep 1) + (setf (wish-call-with-condition-handlers-function *wish*) + (make-call-with-condition-handlers-function debugger-class)) + ;; perform tcl initialisations + (with-ltk-handlers () + (unless remotep + (init-tcl :debug-tcl debug-tcl)) + (when remotep + (send-wish "fconfigure $server -blocking 0 -translation lf -encoding utf-8") + (flush-wish)) + (prog1 (init-wish) + (ensure-timer)))) + ;; By default, we don't automatically create a new connection, because the + ;; user may have simply been careless and doesn't want to push the old + ;; connection aside. The NEW-WISH restart makes it easy to start another. + (restart-case (ltk-error "There is already an inferior wish.") + (new-wish () + :report "Create an additional inferior wish." + (push *wish* *wish-connections*) + (setf *wish* (make-ltk-connection :remotep remotep)) + (apply #'start-wish keys))))) + +;;; CMUCL, SCL, and SBCL, use a two-way-stream and the constituent +;;; streams need to be closed. +(defun close-process-stream (stream) + "Close a 'stream open by 'do-execute." + (when *debug-tk* + (format t "Closing wish stream: ~S~%" stream)) + (ignore-errors (close stream)) + #+(or :cmu :scl :sbcl) + (when (typep stream 'two-way-stream) + (close (two-way-stream-input-stream stream) :abort t) + (close (two-way-stream-output-stream stream) :abort t)) + nil) + +(defun exit-wish () + (with-ltk-handlers () + (let ((stream (wish-stream *wish*))) + (when stream + (remove-input-handler) + (when (open-stream-p stream) + (ignore-errors + (send-wish "exit") + (flush-wish))) + (close-process-stream stream)) + (setf (wish-stream *wish*) nil) + #+:allegro (system:reap-os-subprocess) + (setf *wish-connections* (remove *wish* *wish-connections*)))) + #+:lispworks + (when (wish-remotep *wish*) + (throw 'exit-with-remote-ltk nil)) + (throw *wish* nil)) + +(defun send-wish (text) + (push text (wish-output-buffer *wish*)) + (unless *buffer-for-atomic-output* + (flush-wish))) + +(defun check-for-errors () + (let ((wstream (wish-stream *wish*))) + (when (can-read wstream) + (let ((event (verify-event (read-preserving-whitespace wstream nil nil)))) + (setf (wish-event-queue *wish*) + (append (wish-event-queue *wish*) (list event)))))) + nil) + +;; maximum line length sent over to non-remote Tk +(defparameter *max-line-length* 1000) + +(defun flush-wish () + (let ((buffer (nreverse (wish-output-buffer *wish*)))) + (when buffer + (let ((len (loop for s in buffer summing (length s))) + (*print-pretty* nil) + (stream (wish-stream *wish*))) + (declare (stream stream)) + (incf len (length buffer)) + (setf (wish-output-buffer *wish*) nil) + (handler-bind ((stream-error (lambda (e) (handle-dead-stream e stream))) + #+lispworks + (comm:socket-error (lambda (e) (handle-dead-stream e stream))) + ) + (cond + ((wish-remotep *wish*) + (let ((content (format nil "~{~a~%~}" buffer))) + (format stream "~d ~a~%"(length content) content) + (dbg "~d ~a~%" (length content) content))) + (*max-line-length* + (when (or *debug-buffers* + *debug-tk*) + (format t "buffer size ~a~%" len) (finish-output)) + + (dolist (string buffer) + (loop while (> (length string) *max-line-length*) + do + (let ((sub (subseq string 0 *max-line-length*))) + (setf string (subseq string *max-line-length*)) + (format stream "bt \"~A\"~%" (tkescape2 sub)) + (dbg "bt \"~A\"~%" (tkescape2 sub)))) + (format stream "bt \"~A~%\"~%" (tkescape2 string)) + (dbg "bt \"~A\"~%" (tkescape2 string))) + (format stream "process_buffer~%") + (dbg "process_buffer~%") + ) + (t + (format stream "bt {~D }~%" len) + (dbg "bt {~D }~%" len) + (dolist (string buffer) + (format stream "bt \"~A~%\"~%" (tkescape2 string)) + (dbg "bt \"~A\"~%" (tkescape2 string))) + (format stream "process_buffer~%") + (dbg "process_buffer~%"))) + + (finish-output stream) + + #+nil(loop for string in buffer + do (loop with end = (length string) + with start = 0 + for amount = (min 1024 (- end start)) + while (< start end) + do (let ((string (subseq string start (+ start amount)))) + (format stream "buffer_text {~A}~%" string) + (dbg "buffer_text {~A}~%" string) + (incf start amount))) + (format stream "buffer_text \"\\n\"~%") + (dbg "buffer_text \"\\n\"~%") + finally (progn (format stream "process_buffer~%") + (dbg "process_buffer~%") + (finish-output stream))) + + (setf (wish-output-buffer *wish*) nil)))))) + +(defun handle-dead-stream (err stream) + (when *debug-tk* + (format *trace-output* "Error sending command to wish: ~A" err) + (finish-output)) + (ignore-errors (close stream)) + (exit-wish)) + +(defun format-wish (control &rest args) + "format 'args using 'control as control string to wish" + (send-wish (apply #'format nil control args))) + + +#+nil +(defmacro format-wish (control &rest args) + "format 'args using 'control as control string to wish" + (let ((stream (gensym))) + `(progn + (when *debug-tk* + (format *trace-output* ,control ,@args) + (format *trace-output* "~%") + (finish-output)) + (let ((*print-pretty* nil) + (,stream (wish-stream *wish*))) + (declare (type stream ,stream) + (optimize (speed 3))) + + (format ,stream ,control ,@args) + (format ,stream "~%") + (finish-output ,stream)) + nil))) + + + +;; differences: +;; cmucl/sbcl READ expressions only if there is one more character in the stream, if +;; it is a whitespace its discarded. Lispworks READs the expression as soon as it can +;; be fully read from the stream - no character is discarded +;; so I am printing an additional space after every READable expression printed from tcl, +;; this has to be eaten for read-line from the stream in lispworks (which returns the line +;; ending character, cmucl/sbcl don't) + +(defun read-all(stream) + (declare (stream stream) + #-:lispworks (inline read-char-no-hang)) + (let ((c (read-char-no-hang stream nil nil)) + (s (make-array 256 :adjustable t :element-type 'character :fill-pointer 0))) + (loop + while c + do + (vector-push-extend c s) + (setf c (read-char-no-hang stream nil nil))) + (coerce s 'simple-string))) + +#+(and lispworks mswindows) +(lw:defadvice (mp:process-wait-with-timeout peek-char-no-hang :around) + (whostate timeout &optional function &rest args) + (apply #'lw:call-next-advice + whostate + (if (and (eq timeout 1) + (stringp whostate) + (string= whostate "Waiting for pipe input")) + 0.001 + timeout) + function args)) + +;; set it to like 0.1 to simulate bad networks +;;(defparameter *read-delay* nil) + +;;; read from wish +(defun read-wish () + "Reads from wish. If the next thing in the stream is looks like a lisp-list + read it as such, otherwise read one line as a string." + ;; FIXME: The problem here is that wish sends us error-messages on the same + ;; stream that we use for our own communication. It would be good if we could + ;; get the error-messages (that are presumably written to stderr) onto a separate + ;; stream. The current workaround is based on the observation that wish error + ;; messages always seem to end on a newline, but this may not always be so. + ;; + ;; READ-ALL would be a bad idea anyways, as in that case we could accidentally + ;; snarf a real message from the stream as well, if it immediately followed + ;; an error message. + (flush-wish) + (let ((*read-eval* nil) + (*package* (find-package :ltk)) + (stream (wish-stream *wish*))) + (if (eql #\( (peek-char t stream nil)) + (read stream nil) + (read-line stream nil)))) + + +(defun can-read (stream) + "return t, if there is something to READ on the stream" + (declare (stream stream) + #-:lispworks (inline read-char-no-hang unread-char)) + (let ((c (read-char-no-hang stream))) + (loop + while (and c + (member c '(#\Newline #\Return #\Space))) + do + (setf c (read-char-no-hang stream))) + (when c + (unread-char c stream) + t))) + +(define-condition tk-error (error) + ((message :initarg :message :reader message)) + (:report (lambda (error stream) + (format stream "Tcl/Tk error: ~A" (message error))))) + +(defun verify-event (event) + (cond + ((not (listp event)) + (error "When reading from tcl, expected a list but instead got ~S" event)) + ((eq (first event) :error) + (error 'tk-error :message (second event))) + (t event))) + +(defvar *in-read-event* () + "A list of ltk-connection objects that are currently waiting to read an event.") + +(defun ping-all-wishes () + (dolist (*wish* *in-read-event*) + (format-wish "keepalive"))) + +(defvar *ltk-ping-timer* nil) +(defvar *ping-interval-seconds* nil) + +(defun ensure-timer () + (unless *ltk-ping-timer* + (when *ping-interval-seconds* + #+sbcl + (let ((timer (make-timer (lambda () (ping-all-wishes)) + :name "Ltk ping timer"))) + (schedule-timer timer *ping-interval-seconds* + :repeat-interval *ping-interval-seconds* + :absolute-p nil) + (setf *ltk-ping-timer* timer)) + #+(not sbcl) + nil))) + +(defun tcldebug (something) + (format t "tcl debug: ~a~%" something) + (finish-output)) + +(defun read-event (&key (blocking t) (no-event-value nil)) + "read the next event from wish, return the event or nil, if there is no +event to read and blocking is set to nil" + (or (pop (wish-event-queue *wish*)) + (let ((wstream (wish-stream *wish*))) + (flush-wish) + (if (or blocking (can-read wstream)) + (verify-event + (let ((*in-read-event* (cons *wish* *in-read-event*))) + (read-preserving-whitespace wstream nil nil))) + no-event-value)))) + +(defun read-data () + "Read data from wish. Non-data events are postponed, bogus messages (eg. ++error-strings) are ignored." + (loop + for data = (read-wish) + when (listp data) do + (cond + ((null data) + ;; exit wish + (exit-wish) + (return nil)) + ((eq (first data) :data) + (dbg "read-data: ~s~%" data) + (return (second data))) + ((eq (first data) :debug) + (tcldebug (second data))) + ((find (first data) #(:event :callback :keepalive)) + (dbg "postponing event: ~s~%" data) + (setf (wish-event-queue *wish*) + (append (wish-event-queue *wish*) (list data)))) + ((eq (first data) :error) + (error 'tk-error :message (second data))) + (t + + (format t "read-data problem: ~a~%" data) (finish-output) + )) + else do + (dbg "read-data error: ~a~%" data))) + +(defun read-keyword () + (let ((string (read-data))) + (when (> (length string) 0) + (values (intern #-scl (string-upcase string) + #+scl (if (eq ext:*case-mode* :upper) + (string-upcase string) + (string-downcase string)) + :keyword))))) + + +(defun make-adjustable-string (&optional (string "")) + (make-array (length string) :element-type 'character + :initial-contents string :adjustable t :fill-pointer t)) + +;; Much faster version. For one test run it takes 2 seconds, where the +;; other implementation requires 38 minutes. +(defun tkescape (text) + (unless (stringp text) + (setf text (format nil "~a" text))) + (loop with result = (make-adjustable-string) + for c across text do + (when (member c '(#\\ #\$ #\[ #\] #\{ #\} #\")) + (vector-push-extend #\\ result)) + (vector-push-extend c result) + finally (return result))) + +(defun tkescape2 (text) + (unless (stringp text) + (setf text (format nil "~a" text))) + (loop with result = (make-adjustable-string) + for c across text do + (when (member c '(#\\ #\$ #\[ #\] #\")) + (vector-push-extend #\\ result)) + (vector-push-extend c result) + finally (return result))) + +;;; sanitizing strings: lisp -> tcl (format (wish-stream *wish*) "{~a}" string) +;;; in string escaped : {} mit \{ bzw \} und \ mit \\ + +(defun brace-tkescape (text) + text) +#| + (unless (stringp text) + (setf text (format nil "~a" text))) + (loop with result = (make-adjustable-string) + for c across text do + (when (member c '(#\\ #\{ #\})) + (vector-push-extend #\\ result)) + (vector-push-extend c result) + finally (return result))) + |# + +;; basic tk object +(defclass tkobject () + ((name :accessor name :initarg :name :initform nil) + ) + (:documentation "Base class for every Tk object")) + +;; basic class for all widgets +(defclass widget(tkobject) + ((master :accessor master :initarg :master :initform nil) ;; parent widget or nil + (widget-path :initarg :path :initform nil :accessor %widget-path) ;; pathname to refer to the widget + (init-command :accessor init-command :initform nil :initarg :init-command) + ) + (:documentation "Base class for all widget types")) + +;; creating of the tk widget after creating the clos object +(defmethod initialize-instance :after ((w widget) &key) + (unless (name w) ; generate name if not given + (setf (name w) (create-name)))) + +(defvar *tk* (make-instance 'widget :name "." :path ".") + "dummy widget to access the tk root object") + +;;; tcl -> lisp: puts "$x" mit \ und " escaped +;;; puts [regsub {"} [regsub {\\} $x {\\\\}] {\"}] + +;;; call to convert untility +(defun convert(from to) + (close-process-stream (do-execute "convert" (list from to) t))) + +;;; table used for callback every callback consists of a name of a widget and +;;; a function to call + +(defun add-callback (sym fun) + "create a callback sym is the name to use for storage, fun is the function to call" + (when *debug-tk* + (format *trace-output* "add-callback (~A ~A)~%" sym fun)) + (setf (gethash sym (wish-callbacks *wish*)) fun)) + +(defun remove-callback (sym) + (when *debug-tk* + (format *trace-output* "remove-callback (~A)~%" sym)) + (setf (gethash sym (wish-callbacks *wish*)) nil)) + +(defun callback (sym arg) + "perform the call of the function associated with sym and the args arg" + (let ((fun (gethash sym (wish-callbacks *wish*)))) + (when fun + (apply fun arg)))) + +(defun senddatastring (tclcmd args) + (let ((fmt (format nil "if {[catch {~a} result]} { + puts \"(:error \\\"[escape $result]\\\")\" + } else { + senddatastring $result +}" tclcmd))) + (apply 'format-wish fmt args))) + + +(defun after (time fun) + "after