[plt-scheme] Grafting a console onto universe, hackety hack.

From: Jordan Johnson (jmj at fellowhuman.com)
Date: Wed Jun 2 19:19:09 EDT 2010

Hi guys,

I was talking with one of my students who's implementing a little chat
server (and pig-latin translator) using universe, and naturally it occurred
to me that -- although I've had students implemented a basic command-line
editor earlier in the year (along the lines of what's in HtDP/2e) -- it'd be
nice to have the option of doing this kind of thing without messing around
with console input handling.

So I started playing around with the GUI toolkit, and produced the code
below: a console object and a demo file.  I'm curious about:

     1) whether there's a decent (and minimally intrusive) way to hook into
       universe to trigger events from this console -- polling a state variable
       in the tick handler is kinda icky
     2) whether there's any locking I'd need to do to make it safe for both the
       user-input-callback (see below) and a universe/big-bang event handler
       callback to call append-line as I'm doing below
     3) whether overriding on-char is really an OK way to make a text% object
       "read-only" from the user's perspective
     4) whether there's already some spiffy console-style class I've missed in
       the docs :)


;; --- console-demo.ss ------------------------------------
#lang scheme/gui

(require 2htdp/universe "console.ss")

;; Set up the console window:
(define the-frame
   (let ([new-es (make-eventspace)])
     ;; big-bang takes over the Interactions window's eventspace for use by
     ;; its on-key and on-mouse, so here we create and install a separate
     ;; event space to handle events in our console.
     ;; This means also that
     ;;  (a) we must install a stop-when test to halt the world when the
     ;;      window closes, and
     ;;  (b) after the world halts, (send the-frame show #t) can re-show the
     ;;      frame (but does NOT restart the world).
     (parameterize ([current-eventspace new-es])
       (new console-frame%
            [label "universe test"]
            [width 500]
            [height 400]
            [echo? #f]
             (lambda (s)
               (send the-frame append-line (format "Received ~s." s)))]))))

;; world = Nat, representing the # of seconds elapsed

;; stop? : world -> bool
;; True iff the world should stop.
(define (stop? w) (not (send the-frame is-shown?)))

;; next : world -> world
;; Produces the next world.
;; EFFECT: every five seconds, shows "TICK!" in the console
(define (next w)
   (let ([w2 (add1 w)])
     (when (zero? (modulo w2 5))
       (send the-frame append-line "TICK!"))

;; Run the show:
(define (run w0)
   (send the-frame show #t)
   (big-bang w0
             (on-tick next 1)
             (stop-when stop?)

;; Start it going:
(run 0)

;; -- end console-demo.ss -----------------------------------------

;; -- console.ss --------------------------------------------------
#lang scheme/gui

(provide console-frame%)

;; A frame containing two elements at creation:
;;    - a read-only output text% area
;;    - a text-field% for input
(define console-frame%
   (class frame%

     (init-field user-input-callback  ;; string -> any
                 [echo? #f])
     ;; user-input-callback is called when the user presses Enter, with
     ;; the text-field%'s contents as input.

     ;; The area that displays console output:
     (define output-view
       (new (class text%

              ;; Disable typing in this area:
              (define/override (on-char evt) (void))

              (inherit insert last-position)
              ;; string -> void
              ;; Appends a line at the end (bottom) of the text area.
              (define/public (append-line s)
                (let ([ins (lambda (s) (insert s (last-position)))])
                  (ins "\n")
                  (ins s)))

              ;; boolean -> void
              ;; If this area gets keyboard focus, give it to the input
              ;; area instead.
              (define/override (on-focus on?)
                (when on? (send entry-line focus)))
            [auto-wrap #t]))

     (define ec (new editor-canvas%
                     [parent this]
                     [editor output-view]))

     ;; The area into which the user types their input:
     (define entry-line
       (new text-field%
            [label "text:"]
            [parent this]
            [callback (on-enter
                       (lambda (tf)
                         (let ([s (send tf get-value)])
                           (user-input-callback s)
                           (send tf set-value "")
                           (when echo?
                             (send output-view append-line s))

     ;; string -> void
     ;; Appends the line to the console display.
     (define/public (append-line s) (send output-view append-line s))


;; on-enter : (text-field% -> X) -> (text-field% control-event<%> -> X)
;; Create a callback for a text field, to be triggered only when the user
;; presses Enter.
(define (on-enter f)
   (lambda (tf evt)
     (when (eq? (send evt get-event-type) 'text-field-enter)
       (f tf))))

;; -- end --------------------------------------------------

Posted on the users mailing list.