;;;; ;;;; console.stk -- A simple console written in STk ;;;; ;;;; Copyright © 1998-1999 Erick Gallesio - I3S-CNRS/ESSI ;;;; ;;;; Permission to use, copy, modify, distribute,and license this ;;;; software and its documentation for any purpose is hereby granted, ;;;; provided that existing copyright notices are retained in all ;;;; copies and that this notice is included verbatim in any ;;;; distributions. No written agreement, license, or royalty fee is ;;;; required for any of the authorized uses. ;;;; This software is provided ``AS IS'' without express or implied ;;;; warranty. ;;;; ;;;; Author: Erick Gallesio [eg@unice.fr] ;;;; Creation date: 29-Oct-1998 18:51 ;;;; Last file update: 3-Sep-1999 19:50 (eg) (require "font-lock") (require "butbar") (require "edit") ;;; ;;; Variables which which can be overloaded by the user file ~/.stkvars ;;; (define-module STk (define *show-splash-screen* #t) (define *console-font* '(courier))) ;;; ;;; The rest of the file is in the Tk module ;;; (select-module Tk) (export make-console) (autoload "console-customize" console-customize console-customize-save) ;============================================================================= ; ; Globals ; ;============================================================================= (define *console-version-message* (string-append "STk version" (version) "\n(Tk version is " *tk-patch-level* ")\n\n" "Copyright © 1993-1999\nErick Gallesio - I3S-CNRS/ESSI\n" "")) ;============================================================================= ; ; Utilities ; ;============================================================================= (define (bad-port . _) (error "console is not tied to a standard input port")) (define (set-cursor console pos) (let ((pos (if (console 'compare pos "==" "end") "end - 1 chars" pos))) (console 'mark 'set 'insert pos) (console 'tag 'remove 'sel "1.0" "end") (console 'see "insert"))) ;; ;; console-insert -- ;; Insert a string into a text at the point of the insertion cursor. If ;; there is a selection in the text, and it covers the point of the ;; insertion cursor, then delete the selection before inserting. ;; Insertion is restricted to the prompt area. ;; (define (console-insert console s) (unless (zero? (string-length s)) ; 1. Raise window (raise (winfo 'top console)) ; 2. Do text insertion (catch (when (and (console 'compare "sel.first" "<=" "insert") (console 'compare "sel.last" ">=" "insert")) (console 'tag 'remove 'sel "sel.first" "prompt-end") (console 'delete "sel.first" "sel.last"))) (if (console 'compare "insert" "<" "prompt-end") (console 'mark 'set "insert" "end")) (console 'insert "insert" s "input stdin") (console 'see "insert") ; 3. Fontify (idle-fontify console))) ;; ;; console-output -- ;; This routine is called directly by the interpreter to cause a string ;; to be displayed in the console. ;; (define (console-output console string file-type) (console 'insert "output" string file-type) (console 'see "insert")) ;; ;; console-load ;; (define (console-load) (let ((file (Tk:get-open-file :title "Load File"))) (and file (load file)))) ;; ;; console-about ;; (define (console-about) (let* ((top (toplevel '.__cons_about__)) (m (label (& top ".m") :justify "center" :foreground "IndianRed4" :text *console-version-message*)) (img (make-image "STk-big-logo.gif")) (lab (label (& top ".l") :image img :relief "groove" :bd 5)) (q (button (& top ".b") :text "Close" :command (lambda () (delete-image "STk-big-logo.gif") (destroy top))))) (wm 'title top "About STk ...") (grab top) (raise top) (pack lab :padx 20 :pady 20) (pack m :fill "both" :expand #t) (pack q :ipadx 10 :pady 10))) ;; ;; console-splash-screen ;; (define (console-splash-screen) (let* ((width 400) (height 300) (top (toplevel '.__cons_splash__ :bg "white" :relief "solid" :bd 3 :width width :height height)) (m (label (& top ".m") :justify "center" :fg "IndianRed4" :bg "white" :text *console-version-message*)) (img (make-image "STk-big-logo.gif")) (lab (label (& top ".l") :image img :bd 0)) (w (winfo 'screenwidth top)) (h (winfo 'screenheight top)) (kill (lambda () (catch (delete-image "STk-big-logo.gif")) (destroy top)))) (wm 'over top #t) (wm 'geometry top (format #f "+~A+~A" (quotient (- w 400) 2) (quotient (- h 300) 2))) (pack 'propagate top #f) (pack lab m) (bind top "<1>" kill) ;; for the impatients (raise top) (after 2000 kill))) ;; ;; console-logo ;; (define (console-logo console) (let ((l0 (label (& console ".l0") :image (make-image "LineLeft.gif") :bd 0)) (l1 (label (& console ".l1") :image (make-image "STk-tiny-logo.gif"):bd 0)) (l2 (label (& console ".l2") :image (make-image "LineRight.gif") :bd 0))) (console 'insert 'insert "\n") (console 'window 'create "insert" :window l0 :align "baseline") (console 'insert 'insert " ") (console 'window 'create "insert" :window l1 :align "baseline") (console 'insert 'insert " ") (console 'window 'create "insert" :window l2 :align "baseline") (console 'tag 'add "center" "1.0" "insert") (console 'tag 'configure "center" :justify "center") (console 'insert "insert" "\n\n"))) ;; ;; console-invoke -- ;; ;; Processes the command line input. If the command is complete it ;; is evaluated. ;; (define (console-invoke console module stdin stdout stderr) (let* ((cmd "") (mod (or module (%get-selected-module))) (env (module-environment mod)) (stdcons? (eq? stdin (current-input-port)))) ;; Set cmd to be the concatenation of all ranges of text with tag "input" (let ((hd (console 'tag 'ranges "input"))) (while (not (null? hd)) (set! cmd (string-append cmd (console 'get (car hd) (cadr hd)))) (set! hd (cddr hd)))) (if (complete-sexpr? cmd) (begin ;; We have a complete set of expression to evaluate (console 'mark 'set "output" "end") (console 'tag "delete" "input") (with-input-from-string cmd (lambda () (do ((sexpr (read) (read))) ((eof-object? sexpr)) (add-history! console (substring cmd 0 (- (string-length cmd) 1))) (if stdcons? ;; We are on the main console. Directly fill the std buffer (%fill-stdin cmd) ;; Not the standard console. Use redirection (dynamic-wind (lambda () #f) (lambda () (with-input-from-port stdin (lambda () (with-output-to-port stdout (lambda () (with-error-to-port stderr (lambda () (let ((E (eval sexpr env))) (repl-display-result E))))))))) (lambda () (console-prompt console module stdout stderr) (console 'yview :pickplace "insert")))))))) ;; Not a complete sexpr. Indent text (font-lock-indent console "input")))) ;============================================================================= ; ; console-prompt ; ;============================================================================= (define (console-prompt console module stdout stderr) (let ((temp (console 'index "end -1 char")) (mod (or module (%get-selected-module)))) (with-output-to-port stdout (lambda() (with-error-to-port stderr (lambda () (repl-display-prompt mod))))) (console 'mark 'set "output" temp) (set-cursor console "end") (console 'mark 'set "prompt-end" "insert") ; FIXME: obligé de mettre (console 'mark 'gravity "prompt-end" 'left) ; la gravité? (console 'mark 'set "start_fontify" "insert") (console 'mark 'gravity "start_fontify" 'left))) (define console-display-prompt #f) (define (make-console-display-prompt console stdout stderr) (set! console-display-prompt (lambda (module) (console-prompt console module stdout stderr)))) ;============================================================================= ; ; History management ; ;============================================================================= (define (update-history! console h index) (set-widget-data! console (list :hist h :index index))) (define (get-history console) (let ((data (get-widget-data console))) (if data ; we have already an history for this console (values (get-keyword :hist data) (get-keyword :index data)) ; make an empty history for this console (values '() 0)))) (define (add-history! console line) (call-with-values (lambda () (get-history console)) (lambda (h idx) (update-history! console (cons line h) 0)))) (define(follow-history console oper) (call-with-values (lambda () (get-history console)) (lambda (h idx) (if (null? h) "" (let ((r (list-ref h idx))) (update-history! console h (modulo (oper idx 1) (length h))) r))))) (define (previous-history console) (follow-history console +)) (define (next-history console) (follow-history console -)) ;============================================================================= ; ; Init-console-bindings ; ; This is quite unreadable, but who cares? ; ;============================================================================= (define (init-console-bindings console module stdin stdout stderr) ;; Ignore all Alt, Meta, and Control keypresses unless explicitly bound. ;; Otherwise, if a widget binding for one of these is defined, the ;; class binding will also fire and insert the character, ;; which is wrong. Ditto for . (bind console "" "") (bind console "" "") (bind console "" "") (bind console "" "") (bind console "" "") ;; Inserting characters (bind console "" (lambda () (console-insert console "\t") (focus console) 'break)) (bind console "" (lambda () (console 'mark 'set 'insert "end - 1c") (console-insert console "\n") (console-invoke console module stdin stdout stderr) 'break)) (bind console "" (lambda (|A|) (console-insert console |A|) 'break)) ;; Deleting characters (let ((del (lambda (comparison) (idle-fontify console) (if (null? (console 'tag 'nextrange 'sel "1.0" 'end)) (if (console 'compare "insert" comparison "prompt-end") 'break) (console 'tag 'remove 'sel "sel.first" "prompt-end"))))) (bind console "" (lambda () (del "<"))) (bind console "" (lambda () (del "<"))) (bind console "" (lambda () (del "<="))) (bind console "" (lambda () (idle-fontify console) (if (console 'compare "insert" "<" "prompt-end") (console 'mark 'set "insert" "prompt-end")))) (bind console "" (lambda () (idle-fontify console) (if (console 'compare "insert" "<" "prompt-end") 'break))) (bind console "" (lambda () (idle-fontify console) (if (console 'compare "insert" "<=" "prompt-end") 'break)))) ;; Moving around (let ((start (lambda () (idle-fontify console) (let ((pos (if (console 'comp "insert linestart" ">" "prompt-end") "insert linestart" "prompt-end"))) (set-cursor console pos) 'break))) (end (lambda () (idle-fontify console) (set-cursor console "insert lineend") 'break)) (forw (lambda () (if (console 'compare "insert" ">=" "prompt-end") (set-cursor console "insert+1c")) 'break)) (back (lambda () (if (console 'compare "insert" ">=" "prompt-end") (set-cursor console "insert-1c")) 'break)) (nop (lambda () #f))) (bind console "" start) (bind console "" start) (bind console "" end) (bind console "" end) (bind console "" forw) (bind console "" forw) (bind console "" back) (bind console "" back) (bind console "" nop) (bind console "" nop)) ;; History (let ((prev (lambda () (when (console 'compare "insert linestart" "<" "prompt-end") (console 'delete "prompt-end" "end") (console-insert console (previous-history console)) 'break))) (next (lambda () (when (console 'compare "insert linestart" "<" "prompt-end") (console 'delete "prompt-end" "end") (console-insert console (next-history console)) 'break)))) (bind console "" prev) (bind console "" prev) (bind console "" next) (bind console "" next)) (bind console "<>" (lambda () (catch (console-insert console (selection 'get :displayof console))) (fontify-buffer console "prompt-end") 'break)) (bind console "<>" (lambda () (catch (let ((buffer (console 'get "sel.first" "sel.last"))) (clipboard 'clear :displayof console) (clipboard 'append :displayof console buffer) (console 'delete "sel.first" "sel.last"))) 'break)) (bind console "<>" (lambda () (catch (let ((buffer (console 'get "sel.first" "sel.last"))) (clipboard 'clear :displayof console) (clipboard 'append :displayof console buffer))) 'break)) (bind console "<>" (lambda () (catch (let ((clip (selection 'get :displayof console :selection "CLIPBOARD"))) (console-insert console clip) (fontify-buffer console "prompt-end"))) 'break)) (bind console "" (lambda () (bell) (send-signal |SIGINT|))) ;; Use fontification for the console (but call it by hand because the console ;; completely manage text insertion) (make-fontifiable console) (bindtags console (remove "ScmTxt" (bindtags console))) ) ;============================================================================= ; ; init-console ; ;============================================================================= (define (init-console module std-console?) (let* ((top (toplevel (gensym "._cons_") :class "STk")) (console (text (& top ".txt") :background "white" :setgrid #t :font *console-font*)) (sb (scrollbar (& top ".sb") :width 10)) (mb (console-make-menubar top console)) (bb (console-make-buttonbar top console)) (stdin (if std-console? (current-input-port) (open-input-virtual bad-port bad-port bad-port bad-port))) (stdout (open-output-virtual (lambda (c p) (console-output console (string c) "stdout")) (lambda (s p) (console-output console s "stdout")(update 'idle)) #f #f)) (stderr (open-output-virtual (lambda (c p) (console-output console (string c) "stderr")) (lambda (s p) (console-output console s "stderr")(update 'idle)) #f #f))) ;; Associate the scrollbar commands (tk-set! sb :command (lambda l (apply console 'yview l))) (tk-set! console :yscroll (lambda l (apply sb 'set l))) ;; Pack stuff (pack bb :fill "x") (pack console :expand #t :fill "both" :side "left") (pack sb :expand #f :fill "y" :side "left") (wm 'title top (if module (format #f "Console (~A)" (module-name module)) "STk console")) (if std-console? (let ((exit (lambda () (exit 0)))) (wm 'protocol top "WM_DELETE_WINDOW" exit) (bind console "" exit))) (console 'tag 'configure "stdin" :foreground "black") (console 'tag 'configure "stdout" :foreground "midnightblue") (console 'tag 'configure "stderr" :foreground "#ff2e2f") ;; i.e. "DarkRed" (console 'mark 'set "output" (console 'index "end - 1 char")) (set-cursor console "end") (console 'mark 'set "prompt-end" "insert") (console 'mark 'gravity "prompt-end" "left") (init-console-bindings console module stdin stdout stderr) (if std-console? (begin (if *show-splash-screen* (console-splash-screen)) (if *print-banner* (console-logo console)) (%change-standard-ports stdin stdout stderr) (make-console-display-prompt console stdout stderr)) (console-prompt console module stdout stderr)) (focus console) console)) (define (console-make-buttonbar parent txt) (let* ((f (frame (& parent ".butbar") :relief "ridge" :bd 1))) (make-button-bar f (list 5 (list "tb_console.gif" "Open New Console" make-console) (list "tb_edit.gif" "Open New Editor" ed) (list "tb_customize.gif" "Customize Environment" (lambda () (console-customize))) ; delayed to avoid autoload (list "tb_fileopen.gif" "Load File" console-load) 20 (list "tb_copy.gif" "Copy" (lambda () (event 'gen txt "<>"))) (list "tb_paste.gif" "Paste" (lambda () (event 'gen txt "<>"))) (list "tb_cut.gif" "Cut" (lambda () (event 'gen txt "<>"))) 20 (list "tb_info.gif" "Help on Console" (lambda () (help "make-console"))))) f)) (define (make-console-in) (define name ".__cons_module_chooser") (define (create) (catch (make-console :module (string->symbol (selection 'get))) (after 'idle (lambda () (destroy name))))) ;;;; ;;;; make-console-in starts here ;;;; (destroy name) (let* ((all (sort (map module-name (all-modules)) (lambda (x y) (stringstring x) (symbol->string y))))) (top (toplevel name)) (lab (label (& top ".lab") :text "Choose a module for new console")) (f0 (frame (& top ".f0"))) (lb (listbox (& f0 ".lb") :bg "white" :fg "RoyalBlue" :width 50)) (sb (scrollbar (& f0 ".sb") :width 10)) (f1 (frame (& top ".f1") :relief "ridge" :bd 2)) (doit (button (& f1 ".doit") :text "Create console" :command create)) (quit (button (& f1 ".quit") :text "Cancel" :command (lambda () (destroy top))))) (wm 'title top "Choose module") ;; Fill in the listbox (apply lb 'insert 0 all) ;; Pack everybody (pack lab :expand #f :pady 10 :padx 20 :anchor 'w) (pack lb :fill 'both :side 'left :expand #t) (pack sb :fill 'y :side 'right) (pack f0 :padx 20 :pady 10 :expand #t :fill 'both) (pack doit quit :side 'left :padx 3 :pady 3) (pack f1 :expand #f :fill 'x) ;; Add command to connect the scrollbar (tk-set! lb :yscrollcom (lambda l (apply sb 'set l))) (tk-set! sb :command (lambda l (apply lb 'yview l))) ;; Add binding such as double click on the list create a console (bind lb "" create))) (define (console-make-menubar top console) (let* ((f (frame (& top ".f") :relief "ridge" :bd 1)) (b (make-bordered-frame f)) (file (menubutton (& b ".file") :text "File")) (edit (menubutton (& b ".edit") :text "Edit")) (conf (menubutton (& b ".conf") :text "Customize")) (hlp (menubutton (& b ".help") :text "Help"))) ;; File Menu (let ((m (menu (& file ".m") :tearoff #f))) (m 'add 'command :label "Load ..." :command console-load) (m 'add 'separator) (m 'add 'command :label "New Console" :command make-console) (m 'add 'command :label "New Console in ..." :command make-console-in) (m 'add 'separator) (m 'add 'command :label "Hide Console" :command (lambda () (wm 'iconify top))) (m 'add 'command :label "Close Console" :command (lambda () (destroy top))) (m 'add 'command :label "Exit STk" :command (lambda () (exit 0))) (tk-set! file :menu m) (pack file :side "left")) ;; Edit Menu (let ((m (menu (& edit ".m") :tearoff #f))) (m 'add 'command :label "Cut" :accel "Ctrl-X" :command (lambda () (event 'gen console "<>"))) (m 'add 'command :label "Copy" :command (lambda () (event 'gen console "<>"))) (m 'add 'command :label "Paste" :accel "Ctrl-V" :command (lambda () (event 'gen console "<>"))) (m 'add 'command :label "Clear" :accel "Del" :command (lambda () (event 'gen console "<>"))) (m 'add 'separator) (m 'add 'command :label "Flush Console" :command (lambda () (console 'delete "1.0" "end"))) (tk-set! edit :menu m) (pack edit :side "left")) (let ((m (menu (& conf ".m") :tearoff #f))) (m 'add 'command :label "Customize" :command (lambda () (console-customize))) (m 'add 'command :label "Save Configuration" :command (lambda () (console-customize-save))) (tk-set! conf :menu m) (pack conf :side "left")) ;; Help Menu (let ((m (menu (& hlp ".m") :tearoff #f))) (m 'add 'command :label "STk" :command (lambda () ; Indirect to avoid (help))) ; autoloads (m 'add 'command :label "Console" :command (lambda () (help "make-console"))) (m 'add 'separator) (m 'add 'command :label "About" :command console-about) (tk-set! hlp :menu m) (pack hlp :side "right")) (pack f :fill "x" :side "top"))) ;============================================================================= ; ; make-console ; ;============================================================================= (define (make-console . args) (let ((module (get-keyword :module args #f))) (when module (if (symbol? module) (set! module (find-module module))) (if (not (module? module)) (error "make-console: bad module ~S" module))) (init-console module #f))) (define (%make-standard-console) ;; Don't use try-load because ~ can yiel an error on Windows if HOME is ;; unset (and console is unproperly initialized in this case). (catch (load "~/.stkvars")) (init-console #f #t)) (provide "console")