OF3 <--> DT3 Script (and back again?)

Hello,

I’m looking to create a script to integrate OF3 and DevonThink a bit better so that client projects and client reference material can be created faster and organized more consistently.

I would like to either

  1. Automatically replicate a OF3 Project name as a new sub-group in a DT3 database/group default location, with cross links linking the OF3 project to the DT3 sub-group (and vice versa)
    or
  2. Automatically replicate a DT3 group name as a new project in OF3, with cross links linking the OF3 project to the DT3 sub-group (and vice versa)
    or
  3. best case Open a dialogue box asking the project name, then the script creates both the OF3 Project and the DT3 group in the default location

Does anyone know of anything like this floating out there?

I found this at @rosemaryjayne (Rosemary Orchard) website for shortcuts on iOS which is very cool, but I’m looking for something on Mac. https://rosemaryorchard.com/blog/managing-my-reference-material-with-devonthink-omnifocus-and-shortcuts/

I also found this post in these forums that may be close to what I want, but it is for OF2 and seems to incorporate omnioutliner for some reason? @unlocked2412 seems to have been the hero on this post. Integrating Omnifocus 2 with Devonthink Pro (Rob Trew scripts)

I don’t mind doing some leg work, and I’ve been meaning to learn some AppleScript, but I really don’t know where to start and can’t imagine that someone else hasn’t already had a need for this.

Many thanks and stay healthy!
Troy

1 Like

What should happen in case a DT3 Group exists with that same name ?

I used to do this with indexed files. I can’t upload the script file directly so I’ll host it on my site and share it tomorrow.

Try this:
https://axle.design/files/_OmniFocus-Project-Folder-setup.scpt

The gist of it is: it takes a project in OmniFocus, looks for or creates a file folder on the filesystem, looks for that indexed folder in a DEVONthink database, and then links all of the above. The OmniFocus project will get a link to the filesystem folder and the DEVONthink folder, while the script creates a bookmark file that takes you to that OmniFocus project on the filesystem (and therefore in the indexed DEVONthink folder, too).

I haven’t used it in a few months (my system changed), but it used to work perfectly.

Of course, it’s provided as-is—you need to configure a few things (as documented inline in the script). Also, test on test OmniFocus/DEVONthink databases before you run it on your real files/tasks. It doesn’t do anything destructive but still, no one wants to cry over spilled data.

2 Likes

That is a good question. I suppose just to say that a DT3 Group already exists with that name?

I did figure out how to strip down the DT3 “Reminder” script to add an OF3 project and not ask for any due date details. One small step to learning apple script and being able to do both at the same time, LOL.

1 Like

Wow thank you for sharing this. I will review with caution but it looks like there lots here for me to play with and learn from.

I also read your blog post on your site. I really enjoyed it. I’ve been thinking about using the PARA system, which is precisely why I wanted to use a script like this. I’m looking forward to hearing in your future post how you are now working.

Thank you.

1 Like

@unlocked2412 is the genius behind the updates from OF1. The quick movie I made in this post back in OF2/DTPO days with his updates shows how I used the OO file as well as Keyboard Maestro to flit between OF and the group in DEVONthink.

My workflow is almost exactly the same (again, thanks to unlocked2412’s updates), just using OF3/DTP3/OO5/MindNode.

This is very cool and I was able to get it to work, so thank you.

The main challenge I had is the use of indexed folders as the basis. I’ve had bad luck with indexing (user errors, not DT issues), so I’d rather not use indexed folders as the basis of the workflow.

But I learned a lot from playing with this so thank you! Looking forward to more posts on your blog.

1 Like

Hi @TheWart,

I watched your video and this looks very cool. I tried to adopt your script and incorporate some changes from @bkruisdijk’s version of it, but getting stuck at an error around a “reference url”.

I’m using the same tools as you: OF3/DTP3/OO5/MindNode do you have a version of the script you can share that incorporates the change to your workflow?

Many thanks,

Okay, @TheWart, I realized that I had to make a script bundle and I figured that out and amended some code based on @bkruisdijk’s script.

I really like this, if a bit overkill for my needs. I would still like to incorporate the use of OO5 and MindNode if you don’t mind sharing that part?

I wonder if I can also simplify this. I don’t really need Devonthink to match the tree structure OF3, for instance. For me it would be good enough to create the DTPO3 group with the links in the global inbox, and I can file manually (because my OF3 system requires that I move projects around between folders and I don’t want to have to do that in two systems). My DTPO stucture can just be a flat list of folders if the link back and forth exists in OF3.

FWIW, this is a first draft of a script that prompts the user for a project name and creates a DT Group with a link back (and a corresponding link in the OF Project).

(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        // USER DATA ---
        const strDbName = 'OmniFocus Notes'

        // -------------

        const
            docsPath = standardAdditions().pathTo('home folder'),
            dbSuffix = '.dtBase2',
            strDbPath = docsPath + '/' + strDbName + dbSuffix;


        const dialChoiceLR = strMsg => strTitle => lrDefault => lstButtons =>
            strDefaultButton => strCancelButton => intMaxSeconds => {
                const sa = Object.assign(Application.currentApplication(), {
                    includeStandardAdditions: true
                });
                try {
                    sa.activate;
                    return (() => {
                        // sa :: standardAdditions
                        const dct = sa.displayDialog(strMsg, Object.assign({
                                buttons: lstButtons || ['Cancel', 'OK'],
                                withTitle: strTitle,
                                defaultButton: strDefaultButton || 'OK',
                                cancelButton: strCancelButton || 'Cancel',
                                givingUpAfter: intMaxSeconds || 120
                            },
                            isRight(lrDefault) ? {
                                defaultAnswer: lrDefault.Right
                            } : {}
                        ));
                        return dct.gaveUp ? (
                            Left(dct)
                        ) : Right(dct);
                    })();
                } catch (e) {
                    return Left(e);
                }
            }

        const dtDatabaseFoundOrCreated = strPath => {
            const
                appDT = Application('DEVONthink 3'),
                database = appDT.openDatabase(strPath)
            return database === null ? (
                (() => {
                    const lrChoice = dialChoiceLR('A DEVONthink Database at ' +
                            strPath +
                            ' is going to be created.')
                        ('Create DEVONthink database')(
                            Left()
                        )([])('')('')(20)
                    return isLeft(lrChoice) ? (
                        lrChoice
                    ) : Right(appDT.createDatabase(strPath))
                })()
            ) : Right(database)
        }

        // :: Name String -> OF Item 
        const ofProjectFoundOrCreated = strName => {
            const
                appOF = Application('OmniFocus'),
                oDoc = appOF.defaultDocument,
                projects = appOF
                .defaultDocument
                .flattenedProjects.whose({
                    name: strName
                }),
                proj = appOF.Project({
                    name: strName
                })
            return projects.length === 0 ? (
                (
                    oDoc.projects.push(proj),
                    proj
                )
            ) : projects()[0]
        }
        // :: DT Database -> JS Dict -> DT Record
        const updatedDTGroup = oDatabase => dct => {
            const
                appDT = Application('DEVONthink 3'),
                groups = oDatabase.records.whose({
                    name: dct.name
                })
            return groups.length > 0 ? (
                (groups()[0])
            ) : (appDT.createRecordWith(dct, {
                in: oDatabase.root()
            }))
        }
        // :: OF Item -> JS Dict -> OF Item
        const updatedOFProject = oProj => dct =>
            Object.assign(oProj, dct)

        const ofProjectAsDict = proj => ({
            name: proj.name(),
            URL: 'omnifocus:///task/' + proj.id(),
            type: 'group'
        })

        const dbLR = dtDatabaseFoundOrCreated(strDbPath)

        
        return isLeft(dbLR) ? (
            dbLR.Left
        ) : bindLR(
            dialChoiceLR('Enter Project Title')('OmniFocus <-> DEVONthink')(
                Right('Project Name')
            )([])('')('')(20)
        )(
            compose(
                tpl => updatedOFProject(tpl[0])(tpl[1]),
                secondArrow(
                    compose(
                        group => ({
                            note: group.referenceURL().toString()
                        }),
                        updatedDTGroup(dbLR.Right)
                    )
                ),
                fanArrow(identity)(ofProjectAsDict),
                ofProjectFoundOrCreated,
                dct => dct.textReturned
            )
        )
    };

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

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

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a => b => ({
        type: 'Tuple',
        '0': a,
        '1': b,
        length: 2
    });

    // bindLR (>>=) :: Either a -> 
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        x => fs.reduceRight((a, f) => f(a), x);

    // Compose a function from a simple value to a tuple of
    // the separate outputs of two different functions
    // fanArrow (&&&) :: (a -> b) -> (a -> c) -> (a -> (b, c))
    const fanArrow = f =>
        // Compose a function from a simple value to a tuple of
        // the separate outputs of two different functions.
        g => x => Tuple(f(x))(g(x));

    // identity :: a -> a
    const identity = x =>
        // The identity function. (`id`, in Haskell)
        x;

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

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

    // secondArrow :: (a -> b) -> ((c, a) -> (c, b))
    const secondArrow = f => xy =>
        // A function over a simple value lifted 
        // to a function over a tuple.
        // f (a, b) -> (a, f(b))
        Tuple(xy[0])(
            f(xy[1])
        );

    // uncurry :: (a -> b -> c) -> ((a, b) -> c)
    const uncurry = f =>
        // A function over a pair, derived
        // from a curried function.
        (x, y) => f(x)(y);

    // standardAdditions :: () -> Application
    const standardAdditions = () =>
        Object.assign(Application.currentApplication(), {
            includeStandardAdditions: true
        });


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

I’ve attached the two templates, one for project notes as an OmniOutliner .ooutline file, the other as a MindNode .mindnode file. The former is a simplified version of OO’s built-in “Stylish with Level Style” and the latter is pre-populated with David Allen’s project trigger list which I copied from Getting Things Done.

These two get stored in the script bundle. For posterity (and because only about 0.05% of it is my own work), I’m pasting the current script I use here. There are some comments at the top which I have not updated (references to DTPO v2, for instance):

property pblnJustFolder : false
property pblnUseSyncAsRoot : true
property pblnPreferFTToOmniOO3 : false

-- Robin Trew

-- ver .204 June 19 2011
-- ver .205  [corrects a bug in which a newly created folder was not immediately displayed]
-- ver .206	[unifies the folder and notes scripts, differing only by value of
--			the property pblnJustFolder at the top of the script]
--	Aug 22 2011
-- Ver .208 Still defaults to the DT Database named in pstrDTDB (below)
--			but can use different DT databases for different OF folders
--			(will use the first existing DT database whose POSIX path is found in the note of an enclosing OmniFocus folder
--			[a script for editing OF folder notes can be found at 
--			http://forums.omnigroup.com/showthread.php?t=21942]
-- ver .211 Sep 12 2011
--			Adds option, above - top of script, to make the Sync group the root of all new folders
-- Ver .214 Nov 19 2012 Uses www.FoldingText.com rather than OO3 if pblnPreferFTToOmniOO3=true
-- Ver .216 Now creates .ooutline files.
-- Ver .218 Rich text RTF Link in OF Project Notes
-- Ver .222 Gabriel added MindNode template creation within DTP group

-- Disclaimer
-- This is just a rough draft of something which I have sketched for my own personal use, 
-- and which is provided purely as an illustration of possible approaches to coding.
-- You are free to adapt and reuse any part of it, without any warranties, implied
-- or explicit, as to its behaviour or suitability for use

-- IF property pblnJustFolder : **false**  (see top of script)
-- CREATES/OPENS A PROJECT NOTES FILE (STORED IN DEVONthink 2) FOR THE SELECTED OMNIFOCUS PROJECT.
-- 1.	The project notes file is an OmniOutliner 3 document stored in DEVONthink 2 folder
-- 2.	The Devonthink record contains a hyperlink back to the OmniFocus project
-- 2.	The script ensures that a DT hyperlink to the document in DevonThink is placed in the note field of the project
-- 3.	In the absence of a link to existing notes in DevonThink, 
--		the script will seek or create the 003 file in a DevonThink folder which matches 
--		the folder path of the project in OmniFocus 
--		(and add the hyperlinks from project to notes, and from folder and notes to project)

-- OR - IF property pblnJustFolder : **true** (see top of script)
-- JUST CREATES/OPENS A PROJECT MATERIALS FOLDER (IN DEVONthink 2) 
-- FOR THE SELECTED OMNIFOCUS PROJECT.
-- 1.	The script ensures that a DT hyperlink to the document in DevonThink is placed in 
--		the note field of the project
-- 2.	In the absence of a link to an existing folder in DevonThink, 
--		the script will add the hyperlinks from project to folder, and from folder to project)


-- Acknowledgements
-- Inspired by Jim Harrison's excellent scripts, which use the Finder rather than DEVONthink 2
-- http://jhh.med.virginia.edu/main/OmniFocusScripts
-- The icon attached to this file is from the Float collection by Corey Marion
-- http://iconfactory.com/freeware/preview/flot

-- GLOBAL CONSTANTS
-- Initially assumes that project folders will be maintained in 
-- a database named [UserName]/DEVONthink Databases/Omnifocus.
-- Edit the name and path below to change the location and/or name 
--of the main Projects folder that will contain the individual project folders

property pstrDTDB : "OmniFocus" -- name of default Devonthink Database
property pstrDTsuffix : ".dtBase2"
property pstrDocsPath : "Macintosh HD:Users:YOUR_USERNAME:Databases:" -- (path to documents folder as string) -- path to ~/Documents
property pstrSync : "Active Projects"

property pstrTemplate : "Default"
property pstrMindMapTemplate : "Default"
property pstrOO3Suffix : ".ooutline"
property pstrFTSuffix : ".ft"
property pstrMNSuffix : ".mindnode"

property pstrOFPrefix : "omnifocus:///task/"
property pstrXMLPrefix : "<value key=\"link\">"
property pstrRunDelim : "</lit></run>"
property pstrDTPrefix : "x-devonthink-item://"
property pstrDBPath : "/Users/YOUR_USERNAME/Library/Containers/com.omnigroup.OmniFocus3/Data/Library/Application Support/OmniFocus/OmniFocus Caches/OmniFocusDatabase"
property pstrFolderLinkTitle : "[DEVONthink group]"
property pstrNoteLinkTitle : "[OmniOutliner notes]"
property plngURLchars : 36

on run
	-- IS A PROJECT (OR ONE OF ITS TASKS) SELECTED IN OMNIFOCUS ?
	set {oProject, strProjName, strProjID} to GetSeldProject()
	if oProject is missing value then return
	
	-- IS THERE A RELEVANT LINK IN THE NOTE FIELD OF THE PROJECT ?
	set {strFolderURL, strNotesURL} to DTLinksInNote(oProject)
	
	-- AND IF SO, DOES IT LEAD ANYWHERE ?
	if pblnJustFolder then
		if strFolderURL ≠ "" then if FollowDTLink(strFolderURL) then return
	else
		if strNotesURL ≠ "" then if FollowDTLink(strNotesURL) then return
	end if
	
	-- IN THE ABSENCE OF A LIVE DT LINK, CREATE OR FIND A MATCHING FOLDER PATH IN DT
	tell application id "DNtp"
		set oDTFolder to my GetParallelFolder(oProject)
		set group_name to name of oDTFolder
		set group_reference_URL to reference URL of oDTFolder
		if pblnJustFolder then
			set strDTLink to reference URL of oDTFolder
		else
			-- WHICH NOTE APP ARE WE USING - FOLDINGTEXT OR OO3 ?
			set blnFT to pblnPreferFTToOmniOO3 and my isAppInstalled("com.foldingtext.FoldingText")
			if blnFT then
				set strNoteSuffix to pstrFTSuffix
			else
				set strNoteSuffix to pstrOO3Suffix
			end if
			set strMindMapSuffix to pstrMNSuffix
			
			-- CREATE OR FIND A NOTE FILE FOR THIS PROJECT
			set strNoteName to "• " & strProjName
			set recNotes to my GetNotes(oDTFolder, strNoteName, strNoteSuffix, strMindMapSuffix, strProjID)
			set strDTLink to reference URL of recNotes -- FIX
		end if
		my UpdateOFNote(oProject, group_name, group_reference_URL, strNoteName, strDTLink) -- MODIFICATION
	end tell
	
	-- PLACE AN RTF-FORMATTED DT LINK TO THE NEW OR PRE-EXISTING NOTES  IN THE NOTES FIELD OF THE PROJECT
	if pblnJustFolder then
		set strLinkTitle to pstrFolderLinkTitle
	else
		set strLinkTitle to pstrNoteLinkTitle
	end if
	
	-- AND FOLLOW THE LINK TO THE FOLDER OR NOTES DOCUMENT IN DT2
	FollowDTLink(strDTLink)
	tell application id "DNtp" to activate
end run

-- MODIFICATION
on UpdateOFNote(the_project, group_name, group_url, rec_name, rec_url)
	-- BUILD NOTE
	set the_note to "[DEVONthink group] " & group_name & linefeed & linefeed & "[OmniOutliner notes] " & rec_name & linefeed & linefeed & "================================================================================"
	tell application "OmniFocus"
		tell front document
			set note of the_project to the_note
			tell (note of the_project)
				set value of attribute "link" of style of paragraph 1 to group_url
				set value of attribute "link" of style of paragraph 3 to rec_url
			end tell
		end tell
	end tell
end UpdateOFNote

-- Check whether an app is installed e.g. isAppInstalled("com.foldingtext.FoldingText")
-- for http://www.foldingtext.com
on isAppInstalled(strBundleCode)
	try
		tell application "Finder"
			name of (application file id strBundleCode) ≠ ""
		end tell
	on error
		return false
	end try
end isAppInstalled

-- Return the first project selected in the Omnifocus GUI
on GetSeldProject()
	tell application id "OFOC"
		tell front document
			-- GET THE FIRST SELECTED PROJECT (CONTENT OR SIDEBAR)
			if (count of document windows) < 1 then return {missing value, "", ""}
			tell front document window
				set oProject to missing value
				repeat with oPanel in {content, sidebar}
					set lstSelns to (value of selected trees of oPanel where (class of value = task) or (class of value = project))
					if (count of lstSelns) > 0 then
						set oProject to first item of lstSelns
						exit repeat
					end if
				end repeat
				if oProject is missing value then return {missing value, "", ""}
				if class of oProject = task then set oProject to containing project of oProject
				tell oProject to return {it, name, id}
			end tell
		end tell
	end tell
end GetSeldProject

on DTLinksInNote(oProject)
	tell application id "OFOC"
		-- DOES ITS NOTE CONTAIN A DEVONTHINK URL ?
		set strQuery to "select CAST(notexmldata as text) from task where persistentIdentifier = \"" & (id of oProject) & "\""
		set strNoteXML to do shell script "sqlite3 " & quoted form of pstrDBPath & space & quoted form of strQuery
		set strLink to pstrXMLPrefix & pstrDTPrefix
		set blnOpened to false
		if strNoteXML contains strLink then
			set {strDlm, my text item delimiters} to {my text item delimiters, pstrRunDelim}
			set lstRuns to text items of strNoteXML
			set my text item delimiters to "<lit>"
			set {blnFolder, blnNote} to {false, false}
			set {strFolderURL, strNotesURL} to {"", ""}
			
			set strStart to text 1 thru 2 of pstrNoteLinkTitle
			repeat with oRun in lstRuns
				set lstSections to text items of oRun
				if length of lstSections > 1 then
					set strLabel to item -1 of lstSections
					set strPreamble to item -2 of lstSections
					
					if strLabel begins with strStart then
						if not blnFolder then
							if strLabel contains "folder" then
								set strFolderURL to my parseLink(strPreamble)
								if strFolderURL ≠ "" then set blnFolder to true
							end if
						end if
						if not blnNote then
							if strLabel contains "notes" then
								set strNotesURL to my parseLink(strPreamble)
								if strNotesURL ≠ "" then set blnNotes to true
							end if
						end if
					end if
					if blnFolder and blnNote then exit repeat
				end if
			end repeat
			
			set my text item delimiters to strDlm
			return {strFolderURL, strNotesURL}
		else
			if note of oProject = "" then set note of oProject to space -- (seems to prepare note for easier opening later)
			return {"", ""}
		end if
	end tell
end DTLinksInNote

on parseLink(strXML)
	set {strDlm, my text item delimiters} to {my text item delimiters, pstrXMLPrefix & pstrDTPrefix}
	set lstParts to text items of strXML
	set strURL to ""
	if length of lstParts > 1 then
		set my text item delimiters to "</value>"
		set strURL to first text item of item 2 of lstParts
		if length of strURL = plngURLchars then
			set strURL to pstrDTPrefix & strURL
		else
			set strURL to ""
		end if
	end if
	set my text item delimiters to strDlm
	return strURL
end parseLink

on FollowDTLink(strURL)
	if strURL ≠ "" then
		set blnOpened to (do shell script "open " & quoted form of strURL) = ""
		if blnOpened then
			tell application id "DNtp" to activate
			return true
		else
			return false
		end if
	else
		return false
	end if
end FollowDTLink


on GetParallelFolder(oProject)
	set {strPath, strFolderDB, strProject} to GetProjPath(oProject)
	tell application id "DNtp"
		
		-- CHOOSE THE TARGET DATABASE
		-- EITHER FROM A PATH IN THE ENCLOSING FOLDER, OR FROM THE DEFAULT
		if strFolderDB ≠ "" then
			set strDb to strFolderDB
		else
			set strDb to (POSIX path of pstrDocsPath) & pstrDTDB & pstrDTsuffix
		end if
		
		set oDb to open database strDb
		if oDb is missing value then
			set oAnswer to display dialog "Create new Devonthink database at \"" & strDb & "\" ?" buttons {"Cancel", "OK"} default button 1
			if the button returned of oAnswer is "Cancel" then return
			set oDb to create database strDb
		end if
		
		-- DEPENDING ON GLOBAL SETTING, OPTIONALLY CREATE NEW FOLDER WITHIN SYNC GROUP
		if pblnUseSyncAsRoot then
			set oLocn to create location pstrSync & strPath in oDb
		else
			set oLocn to create location strPath in oDb
		end if
		set URL of oLocn to pstrOFPrefix & (id of oProject)
		return oLocn
	end tell
end GetParallelFolder

on GetProjPath(oProject)
	tell application id "OFOC"
		set strProject to name of oProject
		set strPath to strProject
		set oContainer to container of oProject
		
		set blnFolderFound to false
		set strFolderDB to ""
		
		set cClass to the class of oContainer
		repeat while cClass is not document
			if not blnFolderFound then
				if cClass is folder then
					set strFolderDB to note of oContainer
					if ((strFolderDB ends with pstrDTsuffix) and my FileExists(strFolderDB)) then set blnFolderFound to true
				end if
			end if
			set strPath to name of oContainer & "/" & strPath
			set oContainer to container of oContainer
			set cClass to the class of oContainer
		end repeat
		return {"/" & strPath, strFolderDB, strProject}
	end tell
end GetProjPath

on GetNotes(oDTFolder, strNoteFile, strNoteSuffix, strMindMapSuffix, strProjectID)
	tell application id "DNtp"
		set lstNoteRecs to children of oDTFolder where name = strNoteFile
		if length of lstNoteRecs > 0 then
			return first item of lstNoteRecs
		else
			-- IMPORT A FRESH TEMPLATE FILE FROM THE SAME FOLDER AS THIS SCRIPT
			set strScriptFolder to POSIX path of (path to me)
			--tell application id "MACS" to set oScriptFolder to container of oThisScript
			
			set strTemplate to strScriptFolder & pstrTemplate & strNoteSuffix
			set strMindMapTemplate to strScriptFolder & pstrMindMapTemplate & strMindMapSuffix
			
			-- Check if OO Template Exists
			if my BundleExists(strTemplate) or my FileExists(strTemplate) then
				set oRec to import strTemplate to oDTFolder
			else
				-- OR FROM INSIDE THE SCRIPT BUNDLE
				set strTemplate to POSIX path of (path to me) & pstrTemplate & strNoteSuffix
				try
					set oRec to import strTemplate to oDTFolder
				on error
					display alert strTemplate & " not found in this script bundle"
					return missing value
				end try
			end if
			
			-- Check if MindNode Template Exists
			if my BundleExists(strMindMapTemplate) or my FileExists(strMindMapTemplate) then
				set mnRec to import strMindMapTemplate to oDTFolder
			else
				-- OR FROM INSIDE THE SCRIPT BUNDLE
				set strMindMapTemplate to POSIX path of (path to me) & pstrTemplate & strNoteSuffix
				try
					set mnRec to import strMindMapTemplate to oDTFolder
				on error
					display alert strMindMapTemplate & " not found in this script bundle"
					return missing value
				end try
			end if
			
			tell oRec
				if it is missing value then return it
				set its name to strNoteFile & " notes" & strNoteSuffix
				set its URL to pstrOFPrefix & strProjectID
			end tell
			
			tell mnRec
				if it is missing value then return it
				set its name to strNoteFile & " trigger list" & strMindMapSuffix
				set its URL to pstrOFPrefix & strProjectID
			end tell
			
			return oRec
		end if
	end tell
end GetNotes

on FileExists(strPath)
	(do shell script ("test -e " & quoted form of strPath & "; echo $?")) = "0"
end FileExists

on BundleExists(strPath)
	(do shell script ("test -d " & quoted form of strPath & "; echo $?")) = "0"
end BundleExists

The script I use in the OF toolbar to jump to the OO notes in DTP once I’ve run the above bundle:

tell application "OmniFocus"
	tell front document
		tell front document window
			set lst_values to (value of selected trees of content) whose (class of value = task) or (class of value = project)
			if (count of lst_values) = 0 then
				set lst_values to (value of selected trees of sidebar) whose (class of value = task) or (class of value = project)
				if (count of lst_values) = 0 then
					display notification "Select a project or task"
					return
				end if
			end if
			set the_value to item 1 of lst_values
			if class of the_value = task then
				set the_project to containing project of the_value
			else
				set the_project to the_value
			end if
		end tell
		set note_ref to a reference to (note of the_project)
		tell note_ref
			set oo_link to value of attribute "link" of style of paragraph 3
		end tell
	end tell
end tell

tell application "System Events"
	open location oo_link
end tell

The script I use in the OF toolbar to jump to the DTP group once I’ve run the above bundle:

tell application "OmniFocus"
	tell front document
		tell front document window
			set lst_values to (value of selected trees of content) whose (class of value = task) or (class of value = project)
			if (count of lst_values) = 0 then
				set lst_values to (value of selected trees of sidebar) whose (class of value = task) or (class of value = project)
				if (count of lst_values) = 0 then
					display notification "Select a project or task"
					return
				end if
			end if
			set the_value to item 1 of lst_values
			if class of the_value = task then
				set the_project to containing project of the_value
			else
				set the_project to the_value
			end if
		end tell
		set note_ref to a reference to (note of the_project)
		tell note_ref
			set oo_link to value of attribute "link" of style of paragraph 1
		end tell
	end tell
end tell

tell application "System Events"
	open location oo_link
end tell

OO MN templates.zip (147.5 KB)

2 Likes

Thank you both @unlocked2412 and @TheWart (and also @bkruisdijk and @ryanjamurphy ) for your help with this and your generosity.

Now I have more than I could have asked for and have to pick a path forward. I hope that all this work can also help somebody else too!

@unlocked2412: It took me a moment to realize this is JavaScript? I figured out how to run it and it was perfect - exactly what I asked for. The only question I have is whether it is intended to create a DT database in my Library-Containers-Omnifocus director? If all that is okay, then looks good and I’ll regard that default database as my inbox for new projects and keep the default inbox separate.

@TheWart I may just use these templates in DT3 and import them through a keyboard maestro script as needed because some small projects may not need them.

Really, thank you both for your effort and help. I notice that you both are regular contributors on this forum and I’ll try my best to pay it forward.

2 Likes

You are welcome, @numnumnum.

Indeed, it is JavaScript for Automation. See here:

https://developer.apple.com/library/archive/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/Articles/Introduction.html

Not sure about what you are asking. Could you clarify ?

The script tries to create a new database (if there isn’t one with the specified name) at Documents in you Home folder.

You can change this line (at the top of the script) to suit your needs:

const strDbName = 'OmniFocus Notes'

Hi there, sorry for not being clear.

The script created the new database not at documents but at the following path in the Library/Containers/Omnifocus location in the Finder:

/Users/numnumnum/Library/Containers/com.omnigroup.OmniFocus3.MacAppStore/Data/OmniFocus Notes.dtBase2

It seemed odd, but I thought maybe there was a reason to put it there rather than in my /Users/numnumnum/Databases folder

Thanks for the report. The script should create the database at: /Users/numnumnum/Databases as you say.

How are you running the script ?

I put the script in my Omnifocus Scripts folder and was running from a button in omnifocus until I had set up a keyboard maestro for it. Does that explain the problem?

Does the location where I store the script file matter?

Yes, absolutely. Good information.

I just made a change to the script. Now, it creates the database at you home folder regardless of how you execute it.

Here it is:

(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        // USER DATA ---
        const strDbName = 'OmniFocus Notes'

        // -------------

        const
            docsPath = getHomeDirectory(),
            dbSuffix = '.dtBase2',
            strDbPath = docsPath + '/' + strDbName + dbSuffix;

        const dialChoiceLR = strMsg => strTitle => lrDefault => lstButtons =>
            strDefaultButton => strCancelButton => intMaxSeconds => {
                const sa = Object.assign(Application.currentApplication(), {
                    includeStandardAdditions: true
                });
                try {
                    sa.activate;
                    return (() => {
                        // sa :: standardAdditions
                        const dct = sa.displayDialog(strMsg, Object.assign({
                                buttons: lstButtons || ['Cancel', 'OK'],
                                withTitle: strTitle,
                                defaultButton: strDefaultButton || 'OK',
                                cancelButton: strCancelButton || 'Cancel',
                                givingUpAfter: intMaxSeconds || 120
                            },
                            isRight(lrDefault) ? {
                                defaultAnswer: lrDefault.Right
                            } : {}
                        ));
                        return dct.gaveUp ? (
                            Left(dct)
                        ) : Right(dct);
                    })();
                } catch (e) {
                    return Left(e);
                }
            }

        const dtDatabaseFoundOrCreated = strPath => {
            const
                appDT = Application('DEVONthink 3'),
                database = appDT.openDatabase(strPath)
            return database === null ? (
                (() => {
                    const lrChoice = dialChoiceLR('A DEVONthink Database at ' +
                            strPath +
                            ' is going to be created.')
                        ('Create DEVONthink database')(
                            Left()
                        )([])('')('')(20)
                    return isLeft(lrChoice) ? (
                        lrChoice
                    ) : Right(appDT.createDatabase(strPath))
                })()
            ) : Right(database)
        }

        // :: Name String -> OF Item 
        const ofProjectFoundOrCreated = strName => {
            const
                appOF = Application('OmniFocus'),
                oDoc = appOF.defaultDocument,
                projects = appOF
                .defaultDocument
                .flattenedProjects.whose({
                    name: strName
                }),
                proj = appOF.Project({
                    name: strName
                })
            return projects.length === 0 ? (
                (
                    oDoc.projects.push(proj),
                    proj
                )
            ) : projects()[0]
        }
        // :: DT Database -> JS Dict -> DT Record
        const updatedDTGroup = oDatabase => dct => {
            const
                appDT = Application('DEVONthink 3'),
                groups = oDatabase.records.whose({
                    name: dct.name
                })
            return groups.length > 0 ? (
                (groups()[0])
            ) : (appDT.createRecordWith(dct, {
                in: oDatabase.root()
            }))
        }
        // :: OF Item -> JS Dict -> OF Item
        const updatedOFProject = oProj => dct =>
            Object.assign(oProj, dct)

        const ofProjectAsDict = proj => ({
            name: proj.name(),
            URL: 'omnifocus:///task/' + proj.id(),
            type: 'group'
        })

        const dbLR = dtDatabaseFoundOrCreated(strDbPath)

        
        return isLeft(dbLR) ? (
            dbLR.Left
        ) : bindLR(
            dialChoiceLR('Enter Project Title')('OmniFocus <-> DEVONthink')(
                Right('Project Name')
            )([])('')('')(20)
        )(
            compose(
                tpl => updatedOFProject(tpl[0])(tpl[1]),
                secondArrow(
                    compose(
                        group => ({
                            note: group.referenceURL().toString()
                        }),
                        updatedDTGroup(dbLR.Right)
                    )
                ),
                fanArrow(identity)(ofProjectAsDict),
                ofProjectFoundOrCreated,
                dct => dct.textReturned
            )
        )
    };

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

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

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a => b => ({
        type: 'Tuple',
        '0': a,
        '1': b,
        length: 2
    });

    // bindLR (>>=) :: Either a -> 
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        x => fs.reduceRight((a, f) => f(a), x);

    // Compose a function from a simple value to a tuple of
    // the separate outputs of two different functions
    // fanArrow (&&&) :: (a -> b) -> (a -> c) -> (a -> (b, c))
    const fanArrow = f =>
        // Compose a function from a simple value to a tuple of
        // the separate outputs of two different functions.
        g => x => Tuple(f(x))(g(x));

    // identity :: a -> a
    const identity = x =>
        // The identity function. (`id`, in Haskell)
        x;

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

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

    // secondArrow :: (a -> b) -> ((c, a) -> (c, b))
    const secondArrow = f => xy =>
        // A function over a simple value lifted 
        // to a function over a tuple.
        // f (a, b) -> (a, f(b))
        Tuple(xy[0])(
            f(xy[1])
        );

    // uncurry :: (a -> b -> c) -> ((a, b) -> c)
    const uncurry = f =>
        // A function over a pair, derived
        // from a curried function.
        (x, y) => f(x)(y);

    // getHomeDirectory :: IO FilePath
const getHomeDirectory = () =>
    ObjC.unwrap($.NSHomeDirectory());

    // standardAdditions :: () -> Application
    const standardAdditions = () =>
        Object.assign(Application.currentApplication(), {
            includeStandardAdditions: true
        });


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

Does it work when executed from the toolbar ?

2 Likes

Script Editor barfs on this saying:

Expected expression but found “>”.

You would need to set language tab to JavaScript, I think.

2 Likes