Copyright (c) 2012 Laurent Orseau (laurent orseau gmail com)

License: LGPL v3 or higher (

#lang racket/base
(require drracket/tool
         racket/pretty ; for pretty-write
         racket/path ; for filename-extension
         framework ; for preferences (too heavy a package?)
(provide tool@)

#| TODO:
- several dicts per rktd file, for several functions styles.
- fix?: for the shortcuts to work, the menu must have been generated.  
  (but seems that the menus are re-generated more often than this)
- give the filename to the runned script function.

(require planet/util)
(add-hard-link "orseau" "script-plugin.plt" 1 0 (current-directory))
; once the package is ready for upload:
;(remove-hard-link "orseau" "script-plugin.plt" 1 0)
; then do a `raco setup`

(preferences:set-default 'user-script-dir
                         (path->string (build-path (find-system-path 'pref-dir) "user-scripts"))

(define (script-dir)
  (preferences:get 'user-script-dir))

;(displayln (script-dir))
(make-directory* (script-dir))

(define (set-script-dir dir)
  (preferences:set 'user-script-dir (if (path? dir) (path->string dir) dir)))

(define (choose-script-dir)
  (let ([d (get-directory "Choose a directory to store scripts" #f
;    (displayln d)
    (and d (set-script-dir d))))

(define tool@
    (import drracket:tool^)
    (export drracket:tool-exports^)
    (define script-menu-mixin
      (mixin (drracket:unit:frame<%>) ()
        (inherit get-button-panel
        (define (get-the-text-editor)
          ; for a frame:text% :
          ;(define text (send frame get-editor))
          ; for DrRacket:
        (define frame this)
        ;(define props-default ....)
        (define (new-script)
          (define name (get-text-from-user "Script name" "Enter the name of the script:"))
          (when name
            (define script-name (string-append name ".rkt"))
            (define f-script (build-path (script-dir) script-name))
            (define f-prop (build-path (script-dir) (string-append script-name "d")))
            (with-output-to-file f-prop
              (λ _ (pretty-write `((label . ,name)
                                   (functions . transform-input-string)
                                   ;(sub-menu . #f)
                                   (shortcut . #f)
                                   (shortcut-prefix . #f)
                                   (help-string . "Help String")
                                   (output-to . selection) ; outputs the result in a new tab
                                   (active . #t)
            (with-output-to-file f-script
              (λ _ 
                (displayln "#lang racket/base\n\n;; Sample identity function:")
                (for-each pretty-write 
                          '((provide transform-input-string)
                            (define (transform-input-string str)
                (displayln ";; See the manual for more information")
            (edit-script f-script)
            (edit-script f-prop)
        ;; file: path?
        (define (edit-script file)
          (when file
            ; For frame:text% :
            ;(send (get-the-text-editor) load-file file)
            ; For DrRacket:
            (send this open-in-new-tab file)
        (define (open-script)
          (define file (get-file "Open a script" frame (script-dir) #f #f '() 
                                 '(("Racket" "*.rkt"))))
          (edit-script file)
        (define (open-script-properties)
          (define file (get-file "Open a script properties" frame (script-dir) #f #f '() 
                                 '(("Property file" "*.rktd"))))
          (edit-script file)
        ;; f: path?
        (define (run-script fun file output-to)
          ; For frame:text% :
          ;(define text (send frame get-editor))
          ; For DrRacket:
          (define text (get-the-text-editor))
          (define str (send text get-text 
                            (send text get-start-position) 
                            (send text get-end-position)))
          (define ns (make-base-namespace))
          (define str-out
            (eval `(begin 
                     (require (file ,(path->string file)))
                     (,fun ,str)
          (define (insert-to-text text)
            ; Inserts the text, possibly overwriting the selection:
            (send text begin-edit-sequence)
            (send text insert str-out)
            (send text end-edit-sequence))
          ; DrRacket specific:
          (case output-to
             (insert-to-text (get-the-text-editor))] ; get the newly created text
             (insert-to-text text)]
             (message-box "Ouput" str-out this)]
        (define menu-bar (send this get-menu-bar))
        (define scripts-menu 
          (new menu% [parent menu-bar] [label "&Scripts"]
                  ;; remove all scripts items, after the persistent ones:
                  (for ([item (list-tail (send scripts-menu get-items) 4)])
                    (send item delete))
                  ;; add script items:
                  ; the menu-hash holds the submenus, to avoid creating them more than once
                  (define menu-hash (make-hash))
                  (for ([f (directory-list (script-dir))])
                    (let ([f-prop (build-path (script-dir) (string-append (path->string f) "d"))])
                      (when (and (member (filename-extension f) '(#"rkt"))
                                 (file-exists? f-prop))
                        ; read from the property file
                        (with-input-from-file f-prop 
                          (λ _
                            (let loop ([props (read)]) 
                              (when (and (dict? props) (dict-ref props 'active #t))
                                (let*([label (dict-ref props 'label (path->string f))]
                                      [functions (dict-ref props 'functions 'transform-input-string)]
                                      ;[sub-menu (dict-ref props 'sub-menu #f)]
                                      [parent-menu (if (list? functions)
                                                       (hash-ref! menu-hash label
                                                                  (λ _ (new menu% [parent scripts-menu]
                                                                            [label label])))
                                      [shortcut (dict-ref props 'shortcut #f)]
                                      [shortcut-prefix (or (dict-ref props 'shortcut-prefix #f) 
                                      [help-string (dict-ref props 'help-string #f)]
                                      [output-to (dict-ref props 'output-to 'selection)]
                                      [label-functions (if (list? functions) functions (list (list functions label)))]
                                  (for ([fun (map first label-functions)]
                                        [label (map second label-functions)])
                                    (new menu-item% [parent parent-menu] [label label]
                                         [shortcut shortcut] [shortcut-prefix shortcut-prefix]
                                         [help-string help-string]
                                         [callback (λ(it ev)(run-script fun
                                                                        (build-path (script-dir) f)
                                (loop (read))
        (new menu-item% [parent scripts-menu] [label "New Script"]
             [callback (λ _ (new-script))])
        (new menu-item% [parent scripts-menu] [label "Open Script"]
             [callback (λ _ (open-script))])
        (new menu-item% [parent scripts-menu] [label "Open Script Properties"]
             [callback (λ _ (open-script-properties))])
        (new separator-menu-item% [parent scripts-menu])

        ;; the preference panel is automatically added by DrRacket (nice feature!)
           (define pref-panel (new vertical-panel% [parent parent] [alignment '(center center)]
                                   [spacing 10]
                                   [horiz-margin 10]
                                   [vert-margin 10]
           (define dir-panel (new horizontal-panel% [parent pref-panel]))
           (define text-dir (new text-field% [parent dir-panel] [label "Script directory:"]
                                 [init-value (script-dir)]
                                 [enabled #f]))
           (new button% [parent dir-panel] [label "Change script directory"]
                [callback (λ _ (choose-script-dir))])
           (preferences:add-callback 'user-script-dir
                                     (λ(p v)(send text-dir set-value v)))
    (define (phase1) (void))
    (define (phase2) (void))
    (drracket:get/extend:extend-unit-frame script-menu-mixin)