Script: Manipulate DEVONthink records from within iThoughtsX

If you use iThoughtsX you probably have markdown links to DEVONthink records in your maps. Clicking one opens the record. That’s nice if you want to see the records content or do other stuff with it.

As new iThoughtsX user my first approach to learn how iThoughtsX works was to review a whole database in it (split into parts). But clicking each topics link I wanted to act on soon became annoying as there’s too much overhead:

  • click link
  • change records meta data or run a script
  • close window
  • activate iThoughtsX
  • [repeat]

After unsatisfying success with UI scripting I asked the developer if there’s another way and he told me that itmz are just zip files with XML inside.

I made up this nice script which lets me manipulate DEVONthink records from within iThoughtsX. That means instead of clicking one topic by one to get to the linked record it’s possible to do multiple selection in iThoughtsX, then run the script and it applies code to all records at once. It took at least 4 steps to handle 1 record in iThoughtsX. Using the script with 1 record still takes 2 steps: selecting the topic and running the script. But there’s no need to just select 1 topic, we can select as many as we want.

Don’t get me wrong, this is not about saving some clicks. And it’s not “the script”. It’s just a connector between iThoughtsX topics and DEVONthink records. If you use it you most probably will do something else than I will.

The script only creates the connection from a iThoughtsX topic (which holds a DEVONthink markdown link) to the DEVONthink record - what you do with the record(s) from within iThoughtsX is up to you. Could be simple things like setting a label or sometimes not that simpel things like splitting up a database.

The main advantage of manipulating records from within iThoughtsX is that you first don’t do anything with your records. You’re just using a great mind mapping app which happen to have some links to DEVONthink records. You can do all the normal mind mapping stuff, reorganize topics, group them, create links, take notes (even in topics that hold DEVONthink markdown links). When everything is set up and you want to “write back” the result to DEVONthink then it’s only some line of AppleScript and that’s it. Before running your script it’s a mind map. Afterwards it’s a snapshot of changes you applied to some records, some groups or some databases.

See the screenshot, I created a group listing with markdown links, pasted in iThoughtsX, selected some topics and ran the script with set label of thisRecord to 2. As you can see the label was set for those DEVONthink groups. The most basic example I could think of, but as you know DEVONthink has extensive AppleScript support and everything you have in mind can be planned and rearranged in iThoughtsX.

Note that in this early test there was no chance to tell which topics the script ran on once the selection was lost. So changing selected topics text and / or form is a good idea. I use Keyboard Maestro for this but it’s possible with UI scripting too (see example in script).

Ok, some more advanced examples

  • The database I want to review holds stuff which is often outdated and in this database I can tell that from a records name. But I don’t like to read long filenames from screen edge to edge. So I’ll do this in iThoughtsX instead where I can decide how text wraps. I’ll write what I need in this situation into the script, e.g. move records (via selected topics) to trash. When I’ve reviewed all records in iThoughtsX I’ll go back to DEVONthink and every outdated record sits in trash, ready to delete (By the way, the move command doesn’t work the way I thought it does until yesterday. If you’ve selected a record that has replicants move deletes all replicants and moves the record to the destination, so be aware. There’s an optional parameter “from” (move from) but I didn’t try this yet).

  • Really promising is the combination of @cgrunenberg s “Create Listing” script (to be precise @clane47 s and @korm s modifications which I used to “build” (added two lines…) a “Create Listing with markdown links” version on, see below) and this script as you can try different group structures right in iThoughtsX without changing the current structure of a database.

    • Copy a group listing with markdown links in DEVONthink
    • Paste in iThoughtsX
    • Restructure in iThoughtsX
    • As you see fit select the topic whose position in the map you’ve changed
    • Run the script with display group selector and move (or move from, see above)
    • In group selector select the name of the new parent topic and move it to its new location in DEVONthink
  • Link records

    • Create links between topics in iThoughtsX
    • Select all topics that link to each other
    • Run the script with set custom meta data and let it add a records Reference URL as type URL to all linked records.
  • Use records across databases in one map

    • Copy markdown links in database A
    • Paste in iThoughtsX
    • Copy markdown links in database B
    • Create a new database and duplicate to it or use move to merge (parts) of existing databases
    • Or just set label to mark selected topics / records and create a global smart group
  • Create or restructure nested Tags

    • create location
  • I think really anything that can be done with DEVONthinks AppleScript dictionary and that benefits from

    • organizing in mind maps
    • without touching your records at all until you decide to run your script

By the way: the script can be adapted to every app which owns a URL scheme (if this makes sense depends on the apps (Apple)Script support).

If you don’t use markdown yet here’s a script that copies the DEVONthink selection as markdown links.

Script - "Copy markdown links"
- Copy markdown links

tell application id "DNtp"
	try
		set windowClass to class of window 1
		if {viewer window, search window} contains windowClass then
			set currentRecord_s to selection of window 1
		else if windowClass = document window then
			set currentRecord_s to content record of window 1 as list
		end if
		
		if currentRecord_s = {} then
			display notification "Nichts ausgewählt!" with title "DEVONthink"
			return
		end if
		
		if (count of currentRecord_s) = 1 then
			
			set theRecord to item 1 of currentRecord_s
			set the clipboard to "[" & (name of theRecord) & "](" & (reference URL of theRecord) & ")"
			display notification "Markdown-Link kopiert" with title "DEVONthink"
			
		else
			
			set theMarkdownLinksString to ""
			set theCount to 0
			
			repeat with thisRecord in currentRecord_s
				set thisMarkdownLink to "[" & (name of thisRecord) & "](" & (reference URL of thisRecord) & ")"
				set theCount to theCount + 1
				if theCount ≠ (count of currentRecord_s) then
					set theMarkdownLinksString to theMarkdownLinksString & thisMarkdownLink & return
				else
					set theMarkdownLinksString to theMarkdownLinksString & thisMarkdownLink
				end if
			end repeat
			
			set the clipboard to theMarkdownLinksString
			display notification "Markdown-Links kopiert" with title "DEVONthink"
			
		end if
		
	on error error_message number error_number
		if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
	end try
end tell

This is a markdown version of the “Create Listing” script, be aware that it takes the selection of the navigate sidebar.

Script - "Create Listing with markdown links"
-- Create Listing with markdown links

-- Create Listing.
-- Created by Christian Grunenberg on Sun Jul 24 2005.
-- Copyright (c) 2005-2009. All rights reserved.
-- Modified 20130602 to capture only groups
--Modified by @clane47 11_01_2013 to capture only groups to a rich text file
-- Modified by @korm 20131102 to put the listing on the clipboard for pasting elsewhere
-- Additional modification 20131102 to optionally set base group for the listing to the selected group
-- Modified by @pete31 20200309 to use markdown links (e.g. in iThoughtsX)

tell application id "DNtp"
	try
		if not (exists current database) then error "No database is open."
		set theDatabase to the current database
		
		show progress indicator "Creating Listing..."
		
		set listScope to button returned of (display dialog "Scope of Group Listing" buttons {"Whole Database", "Selected Group and Children", "Cancel"} default button 1 cancel button 3 with title "Put a Group Listing on the Clipboard")
		
		if listScope is "Whole Database" then
			set the clipboard to my createListing(children of root of theDatabase, "")
		else if listScope is "Selected Group and Children" then
			set rootGroup to the current group
			set the clipboard to my createListing(children of rootGroup, "")
		else
			return
		end if
		
		hide progress indicator
		
		display notification "Listing in clipboard!" with title "Create Listing"
		
	on error error_message number error_number
		hide progress indicator
		if the error_number is not -128 then display alert "DEVONthink Pro" message error_message as warning
	end try
end tell

on createListing(theseRecords, theTabs)
	local this_record, this_type, this_listing, this_name
	tell application id "DNtp"
		set this_listing to ""
		repeat with this_record in theseRecords
			set this_name to (name of this_record as string)
			set this_refURL to (reference URL of this_record as string)
			set this_type to type of this_record
			if this_type is group then
				set this_listing to this_listing & theTabs & "[" & this_name & "](" & this_refURL & ")" & return
				step progress indicator this_name
				set this_listing to this_listing & my createListing(children of this_record, theTabs & (ASCII character 9))
			end if
		end repeat
	end tell
	return this_listing
end createListing

Ok, the “Manipulate DEVONthink records from within iThoughtsX” script.

Some things to consider:

  • If selection changed we must make sure that iThoughtsX saves this change (otherwise we get the wrong DEVONthink records and that’s something that really shouldn’t happen). I’m not sure if it’s enough to keystroke "s" using command down without a delay. Testing with small maps worked fine, but running the script with set label on a 1000 links map it did not process all selected topics I think, couldn’t reproduce. So a delay may be needed for large® maps. Don’t know yet if there’s something else than keystroke "s" using command down and / or if we can get the write status of display_state.plist via shell - but I hope that it’s possible somehow. Maybe checking the modification date is a way to go?

  • Couldn’t find a way to make System Events XML suite accept data that’s not written to a file. Is there a way? Would this help in this script anyway?

  • Don’t know why but I’m quiete sure (at least) the double repeat part that’s looking for the DEVONthink UUIDs is far from optimal.

  • Make sure you run the script only on DEVONthink markdown links as there’s no error handling yet.

  • Make sure that you do something with the DEVONthink records, the script as posted does nothing with them.

  • Except for some testing I did not use the script but it’s fun

  • Use a test database!


-- Manipulate DEVONthink records from within iThoughtsX

-- How to use: 
-- Copy markdown links in DEVONthink (selected records or group listing)
-- Paste in iThoughtsX
-- [Do stuff in iThoughtsX]
-- Select topics whose DEVONthink records you want to manipulate
-- Run script (but make sure that your code is in the last DEVONthink tell block)


-- Save iThoughtsX and get document path

activate application "iThoughtsX"
tell application "System Events"
	try
		tell process "iThoughtsX"
			keystroke "s" using command down
			-- delay 1.5
			set theLocation to value of attribute "AXDocument" of window 1
		end tell
		
		set theLocation to characters 8 thru -1 in theLocation as string
		set thePath to my decode_Text(theLocation)
		
	on error
		display notification "Kein Dokument gefunden!" with title "iThoughtsX"
		return
	end try
end tell

-- Get selected iThoughtsX UUIDs

try
	set selectedTopicsUUIDsString to do shell script "/usr/libexec/PlistBuddy -c 'Print selected' /dev/stdin <<< $(unzip -p " & quoted form of thePath & " 'display_state.plist')" -- How to make PlistBuddy accept stdin: https://scriptingosx.com/2018/07/parsing-dscl-output-in-scripts/
on error
	display notification "Nichts ausgewählt!" with title "iThoughtsX"
	return
end try

-- Selected iThoughtsX UUIDs as list

set d to AppleScript's text item delimiters
set AppleScript's text item delimiters to ","
set selectedTopicsUUIDs to text items of selectedTopicsUUIDsString
set AppleScript's text item delimiters to d -- always set them back

-- Get content of iThoughtsX mapdata.xml

set theXMLContent to do shell script "unzip -p " & quoted form of thePath & " 'mapdata.xml'"

-- Content of iThoughtsX mapdata.xml as list

set theParagraphs to paragraphs of theXMLContent

-- Get DEVONthink UUIDs 

set theDEVONthinkUUIDs to {}
repeat with thisSelectedTopicsUUID in selectedTopicsUUIDs
	repeat with thisParagraph in theParagraphs
		if thisParagraph contains thisSelectedTopicsUUID then
			set thisParagraph to contents of thisParagraph
			set devon_offset to offset of "x-devonthink-item://" in thisParagraph
			set thisDEVONthinkUUID to characters (devon_offset + 20) thru (devon_offset + 55) in thisParagraph as string
			set end of theDEVONthinkUUIDs to thisDEVONthinkUUID
			exit repeat
		end if
	end repeat
end repeat

-- Get DEVONthink records and [your code]

tell application id "DNtp"
	try
		repeat with thisDEVONthinkUUID in theDEVONthinkUUIDs
			set thisRecord to (get record with uuid thisDEVONthinkUUID)
			
			
			--- your code
			
			
		end repeat
		
	on error error_message number error_number
		if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
	end try
end tell

-- -- Do something to make selected topics stand out (I use Keyboard Maestro for this but it's also possible with UI scriting) e.g. bold text

-- activate application "iThoughtsX"
-- tell application "System Events"
-- tell process "iThoughtsX"
-- click menu 1 of menu bar item 5 of menu bar 1 -- open menu "Text"
-- click menu item 1 of menu 1 of menu bar item 5 of menu bar 1 -- click menu item "Bold"
-- end tell
-- end tell


on decode_Text(theText) -- http://applescript.bratis-lover.net (not available anymore)
	local str
	try
		return (do shell script "/bin/echo " & quoted form of theText & ¬
			" | perl -MURI::Escape -lne 'print uri_unescape($_)'")
	on error eMsg number eNum
		error "Can't urlDecode: " & eMsg number eNum
	end try
end decode_Text


2 Likes

Having a hard time grasping why this is necessary.

Intriguing idea

Would you be willing to share a bit about the nature of your content so we have a use case example as to how it can help?

It’s a database of tweet HTMLs, the content is also in the name (except for images and videos).

DEVONthinks list view doesn’t wrap text and in preview every opened tweet HTML resizes several times, so DEVONthink is not the right app. Did it some time in Tinderbox but compared to iThoughtsX it’s also not the right tool.

On the other hand iThoughtsX is swift, wraps text and lets me collect similar content - exactly what I need for this task. With the script I can then replicate, duplicate, move, set labels or custom meta data etc. to mirror what I’ve done in iThoughtsX to DEVONthink. Wouldn’t make much sense to review in an app where data is locked in.

Using the tweet name for reviewing also has the nice side effect of not getting in contact with comments. This was often enough a problem … If I want a comment I capture it separately

In case you’re trying to run the script I posted in this thread you’ll find that it doesn’t work in DEVONthink 3.6.

That’s due to DEVONthink’s new handling of “invalide arguments”. After the release of DEVONthink 3 I decided to continue to use “search window” in scripts so that DEVONthink 2 users could use them in, well, search windows. With version 3.6 that’s not possible anymore.

If you want to use the script you’ll have to replace this voluminous block …

set windowClass to class of window 1
if {viewer window, search window} contains windowClass then
	set currentRecord_s to selection of window 1
else if windowClass = document window then
	set currentRecord_s to content record of window 1 as list
end if

… with this neat line …

set currentRecord_s to selected records

… which does what the six lines have done. Wow, that’s great! :smiley:

1 Like