JXA: Reading/Using Data from the Clipboard

This discussion started in another thread. It is a very good discussion, but completely off-topic to the other thread. So I moved it here.

ORIGINAL TOPIC:

Rob, thanks for posting this JXA script. I always learn a lot from your scripts.

When I was testing it and trying to understand how it worked, I ran across this bug:
Your test for valid strClip fails if only an image is on the clipboard.

Here's a small snippet from your script:

var a = Application.currentApplication(),
        sa = (a.includeStandardAdditions = true, a),
        strClip = sa.theClipboard();

    if (strClip) 

The (strClip) returns true if there is only an object (and no text) on the clipboard.

Here's a test script you can run to verify this:

'use strict';

var app = Application.currentApplication()
app.includeStandardAdditions = true

//--- COPY ONLY AN IMAGE TO CLIPBOARD ---
//      Then run this script

var clipStr = app.theClipboard();

//--- This is NOT a valid test for the variable containing text ---
//    It also returns true if the variable is an object, like an
//    image on the clipboard.

if (clipStr) {
  console.log ("'clipStr IS TRUE'")
  console.log(typeof clipStr)
}

//---RESULTS---
/* 'clipStr IS TRUE' */
/* object */

Here is a revised script that fixes this issue, as well as adds some comments to help those like me who are still learning JXA.

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  SAVE TEXT ON CLIPBOARD TO FILE
  ------------------------------
  Ver: 2.0    2016-05-11
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

DATE:   May 11, 2016
AUTHOR: Ver 1:  Rob Trew (@ComplexPoint)
        Ver 2:  JMichaelTX
        
CHANGE LOG:

  β€’ Ver 2    2016-05-11 17:49 CT    JMichaelTX
      β€’ Fixed issue where image only on clipboard caused script to fail.
      β€’ Added return if no text was found, to indicate error.
      β€’ Added clarity to code by decompressing some statements, and adding comments
      
REF:
  β€’ Copy to New TXT File? - general - Keyboard Maestro Discourse
  β€’ https://forum.keyboardmaestro.com/t/copy-to-new-txt-file/3689/2
  
REQUIREMENTS:
  β€’ Use first line as file name
  β€’ Save as TEXT file.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

function run() {
 
    var app = Application.currentApplication()
    app.includeStandardAdditions = true

        
    //--- READ CLIPBOARD ---
    var clipStr = app.theClipboard();

    //--- IF TEXT WAS FOUND ON CLIPBOARD, THEN PROCESS ---
    
    if (typeof clipStr === "string") {
    
      //--- SPLIT TEXT BY LINE INTO ARRAY ---
        var clipArray = clipStr.split(/[\n\r]/)
        
        var numLines = clipArray.length
        
      //--- GET FIRST LINE TO USE AS FILE NAME ---  
        var fileNameStr = numLines > 1 ? clipArray[0] : undefined
        
      //--- JOIN REMAINDER OF ELEMENTS INTO STRING FOR FILE CONTENTS ---
        var fileContentsStr = fileNameStr ? clipArray.slice(1).join('\n') : undefined;

      //--- ASK USER FOR FILE NAME/PATH ---
        if (fileContentsStr) {
        
          //--- ACTIVE CURRENT APP TO DISPLAY DIALOG ---
          app.activate
        
            var outputFilePathStr = (app.chooseFileName({
                    withPrompt: 'Save As',
                    defaultName: fileNameStr + '.txt'
                    })
              ).toString();

          //--- OUTPUT TO SELECTED FILE ---
            writeFile(outputFilePathStr, fileContentsStr);
          
            return (outputFilePathStr);
        }
    }  // END if (clipStr)
    
    else {
      return ("ERROR - No text was found on clipboard")
    }
    
    //~~~~~~~~~ END OF MAIN SCRIPT ~~~~~~~~~~~~~~~~~~~~~
    
     //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    function writeFile(pOutputFilePathStr, pFileContentsStr) {
      // writeFile :: FilePath -> String -> IO ()

        $.NSString.alloc.initWithUTF8String(pFileContentsStr)
            .writeToFileAtomicallyEncodingError(
                pOutputFilePathStr, true,
                $.NSUTF8StringEncoding, null
            );
    }  // END function writeFile()
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

}  // END function run()

  • Looks like a useful reading exercise – thanks
  • with trivial scriplets (very shallow nesting) there is an argument for simply letting them choke on occasionally invalid input – the error message is informative and free – can be better use of scripter time :slight_smile:
  • If you do want a type check, I might write something like (strClip && (typeof strClip === 'string')) to screen out empty strings as well as images
function run() {
    // writeFile :: FilePath -> String -> IO ()
    function writeFile(strPath, strText) {
        $.NSString.alloc.initWithUTF8String(strText)
            .writeToFileAtomicallyEncodingError(
                strPath, true,
                $.NSUTF8StringEncoding, null
            );
    }

    var a = Application.currentApplication(),
        sa = (a.includeStandardAdditions = true, a),
        strClip = sa.theClipboard();

    if (strClip && (typeof strClip === 'string')) {
        var lines = strClip.split(/[\n\r]/),
            strHead = lines.length > 1 ? lines[0] : undefined,
            strTail = strHead ? lines.slice(1)
            .join('\n') : undefined;

        return strTail ? (
            writeFile(
                (sa.activate,
                    sa.chooseFileName({
                        withPrompt: 'Save As',
                        defaultName: strHead + '.txt'
                    })
                ).toString(),
                strTail
            ),
            strTail
        ) : undefined;
    }
}

PS if you are in search of any further distraction, you can experiment with applying Object.keys() to the clipboard object.

String contents will yield a simple series of numeric indices (character positions), while graphic contents will yield the keys to specific serialisations:

1 Like

If "distraction" is learning, then I guess I'm always in search of more. :smile:

Thanks. That is interesting and helpful.

One note: this snippet from your script gives me an error that
"clip is not a function":

var clip = sa.theClipboard
var strType = typeof clip()

It works fine if I just use "clip"
Do you have a function named clip() ?

There was a slight shift in object referencing between 10.10 and 10.11

If you look at it in the 10.11 debugger, you will see that sa.theClipboard (and hence clip, the variable name bound to it) is an uncalled function.

(I don’t have a system running 10.10 here)

Source:

function run() {

    var a = Application.currentApplication(),
        sa = (a.includeStandardAdditions = true, a);
    
        
    var clip = sa.theClipboard,
        strType = typeof clip();

    if (clip() && strType !== 'string') {
        var lstTypes = Object.keys(clip());
        

        if (lstTypes.indexOf('PDF ') !== -1) {
            return clip()['PDF ']
        }
    }
}

Thanks, but I'm now running 10.11:
Script Editor 2.8.1 (183.1) on OSX 10.11.4

The Safari Debugger shows clip as an object, NOT as a function:

I figured out why we are seeing different results:

  • You are using var clip = sa.theClipboard,
  • I am using: var clip = sa.theClipboard(),

So, you are setting the variable clip to the FUNCTION theClipboard,
whereas I am setting it to the VALUE of theClipboard

Using your script, but:
// ##CHG: Added () to theClipboard
// ##CHG: Removed () from "clip()"