1 ;; -*- mode: Lisp; Syntax: Common-Lisp; Package: celtk; -*-
3 ;;; Copyright (c) 2006 by Frank Goenninger, Germany.
5 ;;; Permission is hereby granted, free of charge, to any person obtaining a
6 ;;; copy of this software and associated documentation files (the "Software"),
7 ;;; to deal in the Software without restriction, including without limitation
8 ;;; the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 ;;; and/or sell copies of the Software, and to permit persons to whom the
10 ;;; Software is furnished to do so, subject to the following conditions:
12 ;;; The above copyright notice and this permission notice shall be included in
13 ;;; all copies or substantial portions of the Software.
15 ;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 ;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 ;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 ;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 ;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 ;;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 ;;; DEALINGS IN THE SOFTWARE.
23 ;;; ---------------------------------------------------------------------------
24 ;;; $Header: /project/cells/cvsroot/Celtk/fileevent.lisp,v 1.9 2006/11/04 20:53:08 ktilton Exp $
25 ;;; ---------------------------------------------------------------------------
27 ;;; ===========================================================================
29 ;;; ===========================================================================
33 (eval-when (:load-toplevel
:compile-toplevel
)
34 (export '(tk-fileevent
43 ;;; ===========================================================================
44 ;;; TK-FILEEVENT MODEL
45 ;;; ===========================================================================
47 (defmodel tk-fileevent
(widget)
50 :accessor id
:initarg
:id
52 :documentation
"ID of the fileevent instance.")
55 :accessor input-fd
:initarg
:input-fd
56 :initform
(c?
(if (^iostream
)
57 (stream-2-in-fd (^iostream
))))
58 :documentation
"The input/read file descriptor - internal use only.")
63 :initform
(c?
(if (^iostream
)
64 (stream-2-out-fd (^iostream
))))
65 :documentation
"The output/write file descriptor - internal use only.")
68 :accessor in-tcl-channel
:initarg
:in-tcl-channel
69 :initform
(c?
(fd-to-tcl-channel (^tki
) (^input-fd
)))
70 :documentation
"The TCL channel generated from the input file descriptor. - Internal use only.")
73 :accessor out-tcl-channel
:initarg
:in-tcl-channel
74 :initform
(c?
(fd-to-tcl-channel (^tki
) (^output-fd
)))
75 :documentation
"The TCL channel generated from the output file descriptor. - Internal use only.")
78 :accessor in-tcl-ch-name
:initarg
:in-tcl-ch-name
79 :initform
(c?
(if (^in-tcl-channel
)
80 (Tcl_GetChannelName (^in-tcl-channel
))
82 :documentation
"The input TCL channel's name as passed to the fileevent command. - Internal use only.")
85 :accessor out-tcl-ch-name
:initarg
:in-tcl-ch-name
86 :initform
(c?
(if (^out-tcl-channel
)
87 (Tcl_GetChannelName (^out-tcl-channel
))
89 :documentation
"The output TCL channel's name as passed to the fileevent command. - Internal use only.")
92 :accessor iostream
:initarg
:iostream
94 :documentation
"The Lisp stream to be monitored - API: initarg,setf.")
97 :accessor readable-cb
:initarg
:readable-cb
99 :documentation
"The readable callback. A dispatcher function used to call the function supplied via the read-fn slot. - Internal use only.")
102 :accessor writeable-cb
:initarg
:writeable-cb
104 :documentation
"The writeable callback. A dispatcher function used to call the function supplied via the read-fn slot. - Internal use only.")
107 :accessor eof-cb
:initarg
:eof-cb
109 :documentation
"The eof callback. A dispatcher function used to call the function supplied via the eof-fn slot. - Internal use only.")
112 :accessor error-cb
:initarg
:error-cb
114 :documentation
"The error callback. A dispatcher function used to call the function supplied via the error-fn slot. - Internal use only.")
117 :accessor tki
:initarg
:tki
119 :documentation
"The Tcl/Tk Interpreter used. - API: initarg.")
122 :accessor opcode
:initarg
:opcode
123 :initform
(file-event-opcode-cell-rule)
124 :documentation
"The opcode slot is used to control the operaion of the fileevent instance. - Internal use only.")
127 :accessor read-fn
:initarg
:read-fn
129 :documentation
"User supplied function, gets called when iostream is ready for reading. Gets iostream as parameter. - API: initarg, setf")
132 :accessor write-fn
:initarg
:write-fn
134 :documentation
"User supplied function, gets called when iostream is ready for writing. Gets iostream as parameter. - API: initarg, setf")
137 :accessor eof-fn
:initarg
:eof-fn
139 :documentation
"User supplied function, gets called when iostream is EOF. Gets iostream as parameter. - API: initarg, setf (Via default-initarg set to fn default-eof-fn which simply closes the stream).")
142 :accessor error-fn
:initarg
:error-fn
144 :documentation
"User supplied function, gets called when iostream has encountntered an error. Gets iostream and error sting as parameters. - API: initarg, setf (Via default-initarg set to fn default-error-fn which simply closes the stream and signals an error of class tcl-error)."))
147 :id
(gensym "tk-fileevent-")
148 :eof-fn
'default-eof-fn
))
151 ;;; ===========================================================================
152 ;;; CELL RULE: FILE-EVENT/OPCODE
153 ;;; ===========================================================================
155 ;;; Depending on opcode call the appropriate function to handle the various
156 ;;; cases/combinations of input-fd, output-fd, and the previously executed
157 ;;; update operation.
159 (defun file-event-opcode-cell-rule ()
160 "Set the opcode depending on values of input-fd, output-fd, iostream, readable-cb, writeable-cb"
162 ((not (or (^input-fd
) (^output-fd
) .cache
))
165 ((and (^input-fd
) (^iostream
) (^readable-cb
))
166 :update-input-tk-fileevent
)
168 ((and (^output-fd
) (^iostream
) (^writeable-cb
))
169 :update-output-tk-fileevent
)
171 ((not (or (^iostream
) (^input-fd
)))
172 :reset-input-tk-fileevent
)
174 ((not (or (^iostream
) (^output-fd
)))
175 :reset-output-tk-fileevent
)
179 ;;; ===========================================================================
180 ;;; INIT-TK-FILEEVENT - CALLED UPON INITIALIZATION
181 ;;; ===========================================================================
183 (defun init-tk-fileevent (tki)
185 ;; Nop - all init done in observers now.
188 ;;; ===========================================================================
189 ;;; FILEEVENT HELPER METHODS AND FUCTIONS
190 ;;; ===========================================================================
192 (defmethod set-tk-readable ((self tk-fileevent
) ch-name path type
)
195 ;; The code here was aimed at EOF checking after reading...
196 ;; So the API needs rework...
199 ;; (tk-format-now " proc readable {channel path} {
200 ;; # check for async errors (sockets only, I think)
201 ;; if {[string length [set err [fconfigure $channel -error]]]} {
202 ;; error-cb $path $err
206 ;; # Read a line from the channel
207 ;; if {[catch {set line [gets $channel]} err]} {
208 ;; error-cb $path $err
212 ;; if {[string length $line]} {
213 ;; received-cb $path $line
216 ;; if {[eof $channel]} {
222 ;; frgo: Old code snippet:
223 ;; (tk-format-now "proc readable {channel path} { if [ eof $channel ] then { eof-cb $path } else { readable-cb $path } }")
224 ;; (tk-format-now "fileevent ~A readable [list readable ~A ~A]"
229 (trc "tk-set-readable sees ch-name path type" ch-name path type
)
231 "proc readable {channel path type} {
233 if {! [string compare $type \"socket\"]} {
234 if {[string length [set err [fconfigure $channel -error]]]} {
243 catch { if {[eof $channel]} {
250 (tk-format-now "fileevent ~A readable [list readable ~A ~A ~a]"
257 (defmethod set-tk-writeable ((self tk-fileevent
) ch-name path type
)
258 (tk-format-now "proc writeable {channel path type} { if [ eof $channel ] then { eof-cb $path } else { writeable-cb $path } }")
259 (tk-format-now "fileevent ~A writeable [list writeable ~A ~A ~a]"
265 ;;; ===========================================================================
266 ;;; FILEEVENT CONDITIONS
267 ;;; ===========================================================================
269 (define-condition tcl-fileevent-error
(error)
272 ;;; ===========================================================================
273 ;;; OBSERVERS - USED TO SEND UPDATES TO TK LAND
274 ;;; ===========================================================================
276 (defobserver opcode
((self tk-fileevent
))
277 (let ((*tki
* (tki self
)))
280 ((:init-tk-fileevent
)
281 (init-tk-fileevent (tki self
)))
283 ((:update-input-tk-fileevent
)
284 (let* ((channel (in-tcl-channel self
))
286 (ch-name (Tcl_GetChannelName channel
))
287 (ch-type (Tcl_GetChannelType channel
)))
288 (set-tk-readable self
291 (foreign-slot-value ch-type
295 ((:update-output-tk-fileevent
)
296 (let* ((channel (out-tcl-channel self
))
298 (ch-name (Tcl_GetChannelName channel
))
299 (ch-type (Tcl_GetChannelType channel
)))
300 (set-tk-writeable self
303 (foreign-slot-value ch-type
307 ((:reset-input-tk-fileevent
)
311 ((:reset-output-tk-fileevent
)
319 (defobserver in-tcl-channel
((self tk-fileevent
))
320 (let ((*tki
* (tki self
)))
323 (Tcl_RegisterChannel *tki
* new-value
))
324 (if (and old-value
(not new-value
))
326 (tk-format-now "fileevent ~A readable {}"
327 (Tcl_GetChannelName old-value
))
328 (Tcl_UnregisterChannel *tki
* old-value
)))))
330 (defobserver out-tcl-channel
((self tk-fileevent
))
331 (let ((*tki
* (tki self
)))
332 (if (and new-value
(not old-value
))
333 (Tcl_RegisterChannel *tki
* new-value
))
334 (if (and old-value
(not new-value
))
336 (tk-format-now "fileevent ~A writeable {}"
337 (Tcl_GetChannelName old-value
))
338 (Tcl_UnregisterChannel *tki
* old-value
)))))
340 (defobserver readable-cb
((self tk-fileevent
))
342 (tcl-create-command *tki
*
348 (defobserver writeable-cb
((self tk-fileevent
))
350 (tcl-create-command *tki
*
356 (defobserver eof-cb
((self tk-fileevent
))
358 (tcl-create-command *tki
*
364 (defobserver error-cb
((self tk-fileevent
))
366 (tcl-create-command *tki
*
372 ;;; ===========================================================================
373 ;;; HELPER FUNCTIONS - FILE DESCRIPTOR TO STREAM AND CHANNEL
374 ;;; ===========================================================================
376 (defun fd-to-tcl-channel (interp fd
)
379 (let ((channel (Tcl_MakeFileChannel fd
6))) ;; 6 = READ/WRITE
382 (error "*** Tcl error: ~a" (tcl-get-string-result interp
))))))
385 (defun stream-2-out-fd (stream) ;; FRGO: PORTING...
388 (excl:stream-output-fn stream
)
391 (error "STREAM-2-OUT-FD: Not implemented for ~A Version ~A. Sorry."
392 (lisp-implementation-type)
393 (lisp-implementation-version))
396 (defun stream-2-in-fd (stream) ;; FRGO: PORTING...
399 (excl:stream-input-fn stream
)
402 (error "STREAM-2-IN-FD: Not implemented for ~A Version ~A. Sorry."
403 (lisp-implementation-type)
404 (lisp-implementation-version))
407 ;;; ===========================================================================
409 ;;; ===========================================================================
411 (defcallback readable-cb
:int
412 ((clientData :pointer
)
416 (declare (ignore clientData argc interp
))
417 (let* ((path (foreign-string-to-lisp (mem-aref argv
:pointer
1)))
418 (self (gethash path
(dictionary *tkw
*))))
419 (bwhen (fn (^read-fn
))
420 (funcall fn self
:read
)))
421 (values (foreign-enum-value 'tcl-retcode-values
:tcl-ok
)))
423 (defcallback writeable-cb
:int
424 ((clientData :pointer
)
428 (declare (ignore clientData argc interp
))
429 (let* ((path (foreign-string-to-lisp (mem-aref argv
:pointer
1)))
430 (self (gethash path
(dictionary *tkw
*))))
431 (bwhen (fn (^write-fn
))
432 (funcall fn self
:write
)))
433 (values (foreign-enum-value 'tcl-retcode-values
:tcl-ok
)))
435 (defcallback eof-cb
:int
436 ((clientData :pointer
)
440 (declare (ignore clientData interp argc
))
442 (let* ((path (foreign-string-to-lisp (mem-aref argv
:pointer
1)))
443 (self (gethash path
(dictionary *tkw
*))))
444 (bwhen (fn (^eof-fn
))
446 (values (foreign-enum-value 'tcl-retcode-values
:tcl-ok
)))
448 (defcallback error-cb
:int
449 ((clientData :pointer
)
453 (declare (ignore clientData interp argc
))
455 (let* ((path (foreign-string-to-lisp (mem-aref argv
:pointer
1)))
456 (err$
(foreign-string-to-lisp (mem-aref argv
:pointer
2)))
457 (self (gethash path
(dictionary *tkw
*))))
458 (bwhen (fn (^error-fn
))
459 (funcall fn self err$
)))
460 (values (foreign-enum-value 'tcl-retcode-values
:tcl-error
)))
462 ;;; ===========================================================================
463 ;;; MK-FILEEVENT: CONVENIENCE MACRO
464 ;;; ===========================================================================
466 (defmacro mk-fileevent
(&rest inits
)
467 `(make-instance 'tk-fileevent
469 :readable-cb
(get-callback 'readable-cb
)
470 :writeable-cb
(get-callback 'writeable-cb
)
471 :eof-cb
(get-callback 'eof-cb
)
472 :error-cb
(get-callback 'error-cb
)
476 ;;; ===========================================================================
477 ;;; A DEFAULT EOF FUNCTION, USER MAY SUPPLY ANOTHER FUNCTION WHEN MAKING THE
478 ;;; INSTANCE OF TK-FILEEVENT
479 ;;; ===========================================================================
481 (defmethod default-eof-fn ((self tk-fileevent
))
482 ;; Default action: close stream
483 (bwhen (iostream (^iostream
))
484 (with-integrity (:client
`(:variable
,self
))
485 (setf (^iostream
) nil
)
488 ;;; ===========================================================================
489 ;;; A DEFAULT ERROR FUNCTION, USER MAY SUPPLY ANOTHER FUNCTION WHEN MAKING THE
490 ;;; INSTANCE OF TK-FILEEVENT
491 ;;; ===========================================================================
493 (defmethod default-error-fn ((self tk-fileevent
) err$
)
494 (declare (ignorable err$
))
495 (trc "Heya! Error ~a ... :-(" err$
)
496 ;; Default action 1: close stream
497 (bwhen (iostream (^iostream
))
499 (setf (^iostream
) nil
))
500 ;; Default action 2: signal error
501 (signal 'tcl-fileevent-error
))
503 ;;; ===========================================================================
505 ;;; ===========================================================================
507 ;;; With these few lines below we get a simple application with a text widget
508 ;;; that shows data sent to a pipe in that text widget.
510 ;;; The app does this by opening the named pipe for reading. It then waits
511 ;;; for data on the pipe via the Tcl fileevent command. When establishing
512 ;;; the fileevent a set of callbacks is established. The callbacks call
513 ;;; two Lisp functions, depending on the type of channel (read or write.
515 ;;; The callback functions look for the file channel's registered read or
516 ;;; write functions. Those functions are set via the write-fn and read-fn
517 ;;; methods of the tk-fileevent object.
519 ;;; In the test example below we use the read case: the function read-from-pipe
520 ;;; actually reads from the pipe and sends the data to the text widget by
521 ;;; setting the text widgets model value.
523 ;;; In order to use this example please adapt the code below with a
524 ;;; pipe name suitable for you (see the ^^^^^^^^ marks below).
525 ;;; On Unixes you have to create the pipe with the mkfifo command.
529 ;;; Questions welcome...
537 ;;; This is the User Supplied Read Function USRF. USRF has to take care of
538 ;;; closing the channel if it is a file that is read from !!!
539 ;;; The sample supplied here may serve as a template ...
540 (defmethod USRF ((self tk-fileevent
) &optional
(operation :read
))
541 (declare (ignorable operation
))
542 (let ((stream (^iostream
)))
543 (let ((data (read-line stream nil nil nil
)))
544 (trc "*** USRF: data = " data
)
546 (setf (value (fm-other :receive-window
)) data
)
547 (funcall (^eof-fn
) self
)))))
549 (defmodel fileevent-test-window
(window)
553 (mk-stack (:packing
(c?pack-self
))
554 (mk-label :text
"Receive window"
556 (mk-text-widget :id
:receive-window
564 (mk-fileevent :id
:fileevent-test
567 (open "/Users/frgo/dribble.lisp"
568 ;;; Adapt here !!! ^^^^^^^^^^^^^^^^^^^^^^^^^^^
569 :direction
:input
)))))))
571 ;;; Call this function for testing !!
572 (defun test-fileevent ()
573 (trc "-----------------------------------------------------------------------------")
574 (test-window 'fileevent-test-window
)
575 (trc "-----------------------------------------------------------------------------"))
578 (test-window 'fileevent-test-window
)