Jim Harrison's Project Folder script not working in OF2

I’m not a programmer, so I can’t see what’s wrong with this simple script that I’ve been using in OF1 to link directly to mirror project folders on my hard drive (actually a dropbox folder). Anyone else using this script and have a version updated for the new Applescript requirements? Thanks. John. Here’s the script:

--Written by Jim Harrison, Dec 2008 (jhh.med.virginia.edu). May be used, edited and redistributed without restriction.
--Opens folders at a specified location that have the same name as an OmniFocus project that contains the selection
--Edit the name and path below to correspond to the location of the main Projects folder that will contain the individual project folders
set projectsFolderName to "Projects" -- name for main projects folder
set projectsPath to (path to home folder as text) & "Dropbox" & ":" -- path to main projects folder

tell front window of application "OmniFocus" -- get the name of the project containing the current selection
	try
		set theTrees to the selected trees of content
		if the (count of theTrees) is less than 1 then
			set theTrees to the selected trees of sidebar
		end if
		if the (count of theTrees) is less than 1 then
			display dialog "To open a project folder, click on or in a project, task or note before running this script"
			return
		end if
		set theSelection to value of item 1 of theTrees
		if the class of theSelection is folder then
			set thisProjPath to the name of theSelection
			set theGroup to the container of theSelection
		else
			set thisProjPath to the name of the containing project of theSelection
			set theGroup to the container of the containing project of theSelection
		end if
		repeat while the class of theGroup is not document
			set thisProjPath to the name of theGroup & ":" & thisProjPath
			set theGroup to the container of theGroup
		end repeat
	on error
		display dialog "Click on or in a project, task or note before running this script." buttons {"OK"} default button 1
		return
	end try
end tell

tell application "Finder"
	activate
	try
		open folder (projectsPath & projectsFolderName & ":" & thisProjPath & ":")
	on error
		try
			if not (folder (projectsPath & projectsFolderName & ":") exists) then
				set answer to display dialog "Create new main projects folder at " & projectsPath & " called \"" & projectsFolderName & "?\"" buttons {"Cancel", "OK"} default button 1
				if the button returned of answer is "Cancel" then return
				make new folder at projectsPath with properties {name:projectsFolderName}
			end if
			set oldDelimiter to AppleScript's text item delimiters
			set AppleScript's text item delimiters to ":"
			repeat with i from 1 to (count of text items in thisProjPath)
				if i = 1 then
					set containingFolder to projectsFolderName
					set subPath to ""
				else
					set containingFolder to text item (i - 1) of thisProjPath
					set subPath to (text items 1 thru (i - 1) of thisProjPath as text) & ":"
				end if
				if not (folder (projectsPath & projectsFolderName & ":" & subPath & text item i of thisProjPath) exists) then
					set answer to display dialog "Create new folder \"" & text item i of thisProjPath & "\" in the " & containingFolder & " folder?" buttons {"Cancel", "OK"} default button 1
					if the button returned of answer is "Cancel" then return
					make new folder at alias (projectsPath & projectsFolderName & ":" & subPath) with properties {name:text item i of thisProjPath}
				end if
			end repeat
			set AppleScript's text item delimiters to oldDelimiter
			open folder (projectsPath & projectsFolderName & ":" & thisProjPath & ":")
		on error
			return
		end try
	end try
end tell

I think you need the modified version of this script, which you can find here, on the old forums.
It works fine for me in both OF1 and OF2.

Hi edinventurin,

I couldn’t get the script to work. I did exactly as instructed and the script begins to work, but it asks me to create the folder that already exists at the path they ask me to create it at. The script doesn’t seem to see the folder.

I had been using dropbox quite successfully with OF1 with the following line in the script:

set projectsPath to (path to home folder as text) & “Dropbox” & “:” – path to main projects folder

just as written above without any actual modification to the pathname.

So I used the suggested lines to replace these

set projectsDir to (path to home folder as text) & “Dropbox” & “:” – Path to projects folder (change this as you like)

so, really the only change is the new syntax set projectsDir…

but that didn’t work. Instead it asked me to create the Dropbox folder in the same directory as the script itself!

So I used:

set projectsDir to (“Users/[myusername]/”) & “testOmniFocus” – Path to projects folder (change this as you like)

I used “testOmniFocus” so that I would be forced to create a new folder, rather than using the folder path that contains all of my current folders.

When I did this, the script started to work, but asked me to create a folder at that location:

“Please create the folder Users/[myusername]/testOmniFocus and try again.”

So I can’t get the script to work. I’m using OSX Mavericks 10.9.3

What about the variable projectsFolderName? My script has the folder set as follows:

  • set projectsFolderName to "Projects" – name for projects folder (change this as you like)
  • set projectsDir to (path to home folder as text) & "Dropbox" & ":" – Path to projects folder (change this as you like)`

I’m also on 10.9.3.

Yes the Projects folder is as you wrote it. The problem doesn’t seem to be there.

I’ve just ran the script I found on the link I suggested. It does NOT work, exactly as you said.
So I went on that old forum thread I suggested and further down, on page 5, I found a post by user `teobaldo’ which says:

teobaldo
Member
2011-01-23, 11:08 PM
Druido posted a modified version that creates a bookmark file in Finder, pointing to the OF project.

I found code here that can create a link in any RTF-capable application.
Does that include OO?

Follow the 1st link (Druido) and you will find the version I’m using. Sorry not to have pointed out directly there. It’s such a long time I went through this :D

Thanks Ed,
Yes that’s the script that I originally found going through the old forums. I changed the “Reference” to “Dropbox” as instructed by others so that it comes out exactly as you quoted above.

set projectsFolderName to “Projects” – name for projects folder (change this as you like)
set projectsDir to (path to home folder as text) & “Dropbox” & “:” – Path to projects folder (change this as you like)`

It just doesn’t work (for me).

Thanks for all your help. At this point I think I have to abandon this script because I don’t have the time nor the experience to troubleshoot it myself. (I don’t know the Applescript language, not any programming language for that matter – I’m a dinosaur.)

What’s the full path for your Dropbox folder? And for your desired Projects folder?

Hi Ed,
Maybe I’m missing something, or I misunderstand. The code, taken directly from the script file, is as I wrote it in my previous message (I am pasting from the previous message I wrote):

set projectsFolderName to “Projects” – name for projects folder (change this as you like)
set projectsDir to (path to home folder as text) & “Dropbox” & “:” – Path to projects folder (change this as you like)

Am I supposed to change (path to home folder as text) to a real file path? Because the script that I was using in OF1 was exactly as per the two lines of code above.

Thanks.

John

I was just wondering if your paths are not correct. Because I have the exact same behaviour you are having if they aren’t :S

Do I understand you to say that if you use this line:

set projectsDir to (path to home folder as text) & “Dropbox” & “:”

it does not work. But if you use this line:

set projectsDir to (User/[username]/) & “Dropbox” & “:”

it does work?

No. I’m using 1st version. Did not try 2nd.

Are you on Mac OS Mavericks? I was also using the first version successfully with OF1, but it does not work on OF2.

Also, the second version doesn’t work either, although the program asks me to create the Dropbox folder at the correct location – User/[myusername]/Dropbox – but the problem is that the folder is already there. And I can’t delete, rename, or recreate that Dropbox folder because there are a lot of shared folders that would get broken if I did that.

Yes, I’m on 10.9.3, using OmniFocus bought directly from OmniStore.

This is really annoying. When I use the script, it doesn’t let me point to my dropbox folder. Instead, it insists that I create a folder at this location: /Users/[myusername]/Library/Containers/com.omnigroup.OmniFocus2/Data/Dropbox. I really don’t understand why the script does this. Any ideas?

I’m halfway there. I created a symbolic link at the location and now the Project Folder will open if the folder already exists. But if there is no folder, it does not ask to create a new one, as it did with the old script. Instead, it asks the same question as before: Please create a folder at /Users/[myusername]/Library/Containers/com.omnigroup.OmniFocus2/Data/Dropbox.

Very strange.

No ideas… I doubled checked and have it all fine here.

Not using OF myself these days, but FWIW my partner

  • upgraded to Mojave yesterday,
  • found that OF1 no longer worked,
  • also found that she needed to temporarily install OF2 to migrate the data,
  • and finally discovered that the version of Jim Harrison’s script which she had been using didn’t work with OF3.

Here is a first draft of a JavaScript for Automation replacement.

At the top of the script, you need to specify a path to the root folder in the file system which corresponds to the top level of your OF3 sidebar.

The default is: ~/Documents/projects

Health warning – I hope that this might be at least a useful reference or resource for someone, but I’m not yet sure how much it will be supported. It will be supported if my partner continues to manage her work with OF3, which she may well do, but she’s also thinking about migrating the case folders over to the new version of DEVONThink 3, in which case there won’t be a lot of OmniFocus activity in this household.

Finally, remember that if you do save a copy of this from Script Editor to

~/Library/Application Scripts/com.omnigroup.OmniFocus3,

(to experiment with using it in the OF3 toolbar) you will need to ensure that the language tab at top left of Script Editor is set to JavaScript, rather than AppleScript., and use Save As to a .scpt format file.

(() => {
    'use strict';

    // RobTrew @complexpoint 2019
    // https://github.com/RobTrew
    // Ver 0.01


    // fpRoot :: FilePath
    // Folder path for the top level of the Omnifocus 3 sidebar
    const fpRoot = '~/Documents/projects';

    // Finder folder opened or created for
    // the first (if any) selected project/folder in
    // OmniFocus 3

    // main :: IO ()
    const main = () => {
        const
            of3 = Application('OmniFocus'),
            sa = (
                of3.includeStandardAdditions = true,
                of3
            ),
            ws = of3.documents.at(0).documentWindows;

        return bindMay(
            ws.length ? Just(ws.at(0)) : Nothing(),
            w => {
                const sidebar = w.sidebar;
                return bindMay(
                    maybeSelection(w)(sidebar),
                    trees => bindMay(
                        pathFoundorCreated(sa)(
                            filePathFromSidebar(fpRoot)(
                                sidebar
                            )(trees)
                        ),
                        openedFolder
                    )
                );
            }
        );
    };

    // OF PROJECT FOLDER ----------------------------------

    // maybeSelection :: OF Doc Win -> sidebar -> Maybe trees
    const maybeSelection = w => sidebar =>
        find((x => 0 < x.length), [
            sidebar.selectedTrees,
            w.content.selectedTrees
        ]);

    // openedFolder :: FilePath -> Maybe FilePath
    const openedFolder = fp => {
        const finder = Application('Finder');
        finder.open(Path(fp));
        finder.activate();
        return Just(fp);
    };

    // pathFoundorCreated :: Standard Additions ->
    //                       FilePath -> Maybe FilePath
    const pathFoundorCreated = sa => fpPath =>
        doesPathExist(fpPath) ? (
            Just(fpPath)
        ) : (() => {
            sa.activate();
            return either(
                x => Nothing(),
                x => (
                    createDirectoryIfMissingLR(
                        true, fpPath
                    ),
                    Just(fpPath)
                ),
                dialogChoiceLR(sa)(
                    'No existing project folder found',
                    'Create new folder in Finder at:\n\n' +
                    ObjC.unwrap(
                        $(fpPath)[
                            'stringByAbbreviating' +
                            'WithTildeInPath'
                        ]
                    ) +
                    '\n\n?',
                    Left(''),
                    ['Cancel', 'OK'], 'OK', 'Cancel',
                    30
                )
            );
        })();


    // selnProjectId :: OF Node -> ID String
    const selnProjectId = node => {
        const props = node.properties();
        return ['project', 'folder']
            .includes(props.pcls) ? (
                props.id
            ) : props.containingProject.id();
    };

    // filePathFromSidebar :: FilePath ->
    //      OF Selected Trees -> FilePath
    const filePathFromSidebar = fpRoot => sidebar => trees =>
        filePath(fpRoot) + '/' +
        treePath(
            sidebar.descendantTrees.byId(
                selnProjectId(
                    trees.at(0).value()
                )
            )
        ).join('/');

    // treePath :: OF Node -> [String]
    const treePath = node => {
        // In case any node names contain '/'
        const normalized = s =>
            s.replace(/\//g, ':')
        return tail(reverse(cons(
            normalized(node.name()),
            map(t => normalized(t.name() || ''),
                node.ancestorTrees()
            )
        )));
    };

    // GENERIC FUNCTIONS ----------------------------------
    // https://github.com/RobTrew/prelude-jxa

    // Just :: a -> Maybe a
    const Just = x => ({
        type: 'Maybe',
        Nothing: false,
        Just: x
    });

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Nothing :: Maybe a
    const Nothing = () => ({
        type: 'Maybe',
        Nothing: true,
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindMay (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
    const bindMay = (mb, mf) =>
        mb.Nothing ? mb : mf(mb.Just);

    // cons :: a -> [a] -> [a]
    const cons = (x, xs) => [x].concat(xs);

    // createDirectoryIfMissingLR :: Bool -> FilePath ->
    //                               Either String String
    const createDirectoryIfMissingLR = (blnParents, fp) =>
        doesPathExist(fp) ? (
            Right(`Found: '${fp}'`)
        ) : (() => {
            const
                e = $(),
                blnOK = $.NSFileManager.defaultManager[
                    'createDirectoryAtPath' +
                    'WithIntermediateDirectoriesAttributesError'
                ]($(fp)
                    .stringByStandardizingPath,
                    blnParents, undefined, e
                );
            return blnOK ? (
                Right(fp)
            ) : Left(e.localizedDescription);
        })();

    // dialogChoiceLR :: String -> String -> Either String String ->
    //      [String] -> String -> String -> Int -> FilePath
    //          -> Either Dict String
    const dialogChoiceLR = sa => (
        strTitle, strMsg, lrDefault, lstButtons, strDefaultButton,
        strCancelButton, intMaxSeconds, strIconPath
    ) => {
        try {
            sa.activate;
            return (() => {
                // sa :: standardAdditions
                const dct = sa.displayDialog(strMsg, Object.assign({
                    buttons: lstButtons || ['Cancel', 'OK'],
                    defaultButton: strDefaultButton || 'OK',
                    cancelButton: strCancelButton || 'Cancel',
                    withTitle: strTitle,
                    givingUpAfter: intMaxSeconds || 120
                }, isRight(lrDefault) ? {
                    defaultAnswer: lrDefault.Right
                } : {}, typeof strIconPath === 'string' ? {
                    withIcon: Path(strIconPath)
                } : {}));
                return dct.gaveUp ? (
                    Left(dct)
                ) : Right(dct.textReturned);
            })();
        } catch (e) {
            return Left(e);
        }
    };

    // doesPathExist :: FilePath -> IO Bool
    const doesPathExist = strPath =>
        $.NSFileManager.defaultManager
        .fileExistsAtPath(
            $(strPath).stringByStandardizingPath
        );

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = (fl, fr, e) =>
        'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

    // filePath :: String -> FilePath
    const filePath = s =>
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);

    // find :: (a -> Bool) -> [a] -> Maybe a
    const find = (p, xs) => {
        for (let i = 0, lng = xs.length; i < lng; i++) {
            const v = xs[i];
            if (p(v)) return Just(v);
        }
        return Nothing();
    };

    // isRight :: Either a b -> Bool
    const isRight = lr =>
        ('undefined' !== typeof lr) &&
        ('Either' === lr.type) && (undefined !== lr.Right);

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) =>
        (Array.isArray(xs) ? (
            xs
        ) : xs.split('')).map(f);

    // reverse :: [a] -> [a]
    const reverse = xs =>
        'string' !== typeof xs ? (
            xs.slice(0).reverse()
        ) : xs.split('').reverse().join('');

    // showLog :: a -> IO ()
    const showLog = (...args) =>
        console.log(
            args
            .map(JSON.stringify)
            .join(' -> ')
        );

    // tail :: [a] -> [a]
    const tail = xs => 0 < xs.length ? xs.slice(1) : [];

    // MAIN ---
    return main();
})();

3 Likes