Monday, April 12, 2010

A Scheme Copy-and-Paste Hack

The other day I was hacking away, using DrScheme as a scratchpad and a Google Spreadsheet to visualize data.

After way too many rows of data entry, it hit me - I can trivially automate copying values from DrScheme to a Google Spreadsheet. PLT-Scheme gives you access to a clipboard object that you can simply call set-clipboard-string. Using this method, I was able to write a Scheme function that injected data into the system clipboard, which I could then paste into my spreadsheet.

To smooth things out a bit, I wrote a function which takes in lists of lists, and converts them to tab-and-newline-delimited strings, that Google knows how to parse into cells.

The result is that I can now type:

;; ctc is an alias for copy-to-clipboard
(ctc '((Name Rank Serial-Number)
       (Ben Private 123) (Shira General 456)))

and then hit Control-V to get:

Of course, if Ireally wanted to push data to a Google Spreadsheet, there are much cleaner ways to do it. But, this copy and paste hack gave me just the solution I needed.

It's also worth mentioning that I do this sort of thing all the time from the command line. There's a standard Windows command, C:\Windows\System32\clip.exe that allows you to send content to the clipboard via the shell. (In cygwin, I can do: cat /etc/passwd | tr a-z A-Z | clip).

Though, in this case, I was already poking around DrScheme so a it made more scheme to develop a PLT specific solution.

Here's the code:

#lang scheme
(require scheme/gui ; used to get the-clipboard variable
         srfi/26    ; for cut
         )
;;
;; The clipboard functions choke on null strings. So I replace
;; them with this character. Most of the time, you won't need
;; to worry about this
;;
(define null-character-replacement (make-parameter "$"))


(define as-string (cut format "~a" <>))

;;
;; gridify: take in a list of lists and turn it into a tab/newline
;; delimited string, ready to be copied and pasted into a spreadsheet.
;; Also handles the case where the data isn't a lists of lists.
;;
(define (gridify any)
  (if (list? any)
      (string-join (map (compose as-string
                                 (lambda (row)
                                   (if (list? row)
                                       (string-join (map as-string row) "\t")
                                       row)))                                 
                        any) "\n")
      (as-string any)))

;;
;; Copy any content to the clipboard as a string. Note: we call (gridify any)
;; to make the data more clipboard friendly
;;
;; As a convenience, return the data sent in. Useful to allow calling
;; this function in a debugging context.
;;
(define (copy-to-clipboard any)
  (let ([scrubbed (regexp-replace* "[\0]" 
                                   (gridify any) 
                                   (null-character-replacement))])
    (send the-clipboard set-clipboard-string scrubbed 0)
    any))

;;
;; provide a compact alias for (copy-to-clipboard ...)
;;
(define ctc copy-to-clipboard)

(provide/contract
 [copy-to-clipboard (any/c . -> . any/c)]
 [ctc (any/c . -> . any/c)])

3 comments:

  1. Just a side note in case someone is interested:

    On OS X the clipboard copying command is pbcopy and you can use it like this:
    (require scheme/system)

    (define (pbcopy data)
    (define-values (stdout stdin pid stderr proc) (apply values (process "pbcopy")))
    (close-input-port stdout) (close-input-port stderr)
    (display data stdin)
    (close-output-port stdin)
    (proc 'wait))

    ReplyDelete
  2. I've gone on a jag recently, replacing all my uses of "cut" with "curry", which I just think is slicker. (And it's in "scheme", so you don't have to require anything to get it).

    So

    (require srfi/26)
    (define as-string (cut format "~a" <>))

    becomes

    (define as-string (curry format "~a"))

    ReplyDelete
  3. JPC - Thanks for the OS-X tip.

    Offyb1 - plt-scheme never ceases to amaze...look at that, they've added (curry ...). I'll definitely start looking for ways to use it. Though, I do like the explicit nature of (cut ...) which allows for invocations like:

    (map (cut format <> 100 'x)
    '("!~a!~a~" ">~a!~a<"))

    PLT-Sheme also offers a a curried notation of (define ...) which is cool, though I haven't found an ideal use case for it yet.

    ReplyDelete