Tinderbox and Hook

We received some requests for Tinderbox support quite a while ago: Hook with Tinderbox 8.

None of us here at CogSci Apps at the time were Tinderbox users. However, Graham has been delving into it, and should have some scripts to share next month.

The native tinderbox:// url-scheme does most of the work for free,
but it doesn’t record the filepath of the active document.

(It was first conceived for linking within open documents, and any file not open or in the MRU list is hidden to it.).

SO

  1. We can usefully capture the tinderbox:// url itself (possible through osascript in Tinderbox 8), but
  2. we also need to append a ?filepath=<encodedurl> option to it, where encodedurl is derived from the file path of the active Tinderbox document.
  3. Because of this ?filepath addition, which is outside the scope of the tinderbox:// scheme itself, we also need a Hook Open Item script, to handle the document finding, giving it a suitable custom scheme name, perhaps tbx://

For Name and Address, Hook now allows us to write a single ‘Get Address’ script which returns both Name and Addresss in a markdown [Name](Address) format.

As the Get Address script needs to percent-encode the file path, and the Open Item script needs to decode that url-encoding, JavaScript may be a little more straightforward than AppleScript (JS has built in encode and decode functions).

For example, we can, first for the Open Item script:

  1. Enter tbx in the Hook:// document-identifier field at the bottom of the Open Script panel in Hook preferences.
  2. Paste in the whole code below, from the //JavaScript comment line at the top, which Hook requires to identify the script language, down to })(); at the end.
//JavaScript
(() => {
    'use strict';
    const
        parts = "$0".split(/\?filepath=/),
        tbx = Application('Tinderbox 8');
    return (
        tbx.activate(),
        tbx.open(Path(
            decodeURIComponent(
                parts.slice(-1)[0] // Document filepath.
            )
        )),
        Object.assign(
            Application.currentApplication(), {
                includeStandardAdditions: true
            }
        ).openLocation(
            `tinderbox://${parts[0].split('//')[1]}`
        )
    );
})();

and then for the Get Address script.

Paste the the whole of the following (again, including \\JavaScript at the top, down to })(); at the end.

//JavaScript
(() => {
    'use strict';

    const ds = Application('Tinderbox 8').documents;
    return 0 < ds.length ? (() => {
        const 
            doc = ds.at(0),
            note = doc.selectedNote();
        return null !== note ? (
            `[${note.name()}](${
                'tbx' + note.attributes.byName('NoteURL')
                .value().slice('tinderbox'.length) + 
                '?filepath=' + encodeURIComponent(doc.file().toString())
             })`
        ) : '';
    })() : '';
})();

An additional subtlety is the question of whether we want Hook links to restore the Tinderbox view type (map vs outline etc) in addition to the note selection.

In the script above, the TBX NoteURL is read from the corresponding attribute of the selected note. The attribute version of the url always opens a note in outline view.

Alternatively, we can obtain a link which remembers view type through a GUI sub-menu in the Tinderbox app: Note > Copy Note URL, or with the corresponding key-stroke ^⌥⌘U.

I personally don’t do this, for two reasons:

  1. The attribute route makes for a simpler and more solid script (GUI scripting invites the slings and arrows of uncertain timing),
  2. Outline view is the quickest to load, and I find it a good default.

Nevertheless, restoring another view would be perfectly possible, in contexts where it might seem useful, and for earlier versions of Tinderbox (the osacript interface was introduced in ver 8.0), GUI scripting of Note > Copy Note URL would in any case be the only option.

Thanks Rob.

Question:
As I understand it, the scripts above are for the “Open Item” and “Get Address” tinderbox panels in hook under preferences
What about the “Get Name” panel? Does this remain blank?

Thanks in advance
Tom

That’s right – we don’t need a Get Name script if the Get Address script returns a Markdown link pattern which includes a name.

1 Like

Many Thanks Rob for your assistance.

Tom

2 Likes

For reference, AppleScript equivalents (using the Foundation classes for url encoding and decoding) might look something like:

Get Address
(copy whole script, down to end encodedPath)

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

on run
    tell application "Tinderbox 8"
        set ds to documents
        if 0 < length of ds then
            set doc to item 1 of ds
            set oFile to file of doc
            
            if missing value is not oFile then
                set fp to my encodedPath(POSIX path of (oFile as alias)) as string
                
                set oNote to selected note of doc
                if missing value is not oNote then
                    set noteURL to (value of (attribute "NoteURL" of oNote)) as string
                    set strURL to (text (1 + (length of "tinderbox")) thru -1 of noteURL)
                    "[" & name of oNote & "](" & "tbx" & strURL & "?filepath=" & fp & ")"
                else
                    ""
                end if
            else
                "[Hook can't link an unsaved file]()"
            end if
        else
            ""
        end if
    end tell
end run

-- GENERIC 
-- https://github.com/RobTrew/prelude-applescript

-- encodedPath :: FilePath -> Percent Encoded String
on encodedPath(fp)
    tell current application
        (its ((NSString's stringWithString:fp)'s ¬
            stringByAddingPercentEncodingWithAllowedCharacters:(its NSCharacterSet's ¬
                URLPathAllowedCharacterSet))) as string
    end tell
end encodedPath

Open Item
(Copy whole script, down to end splitOn)

use framework "Foundation"
use scripting additions

on run
    set xs to splitOn("?filePath=", "$0")
    set ys to splitOn("//", item 1 of xs)
    
    tell application "Tinderbox 8"
        activate
        open (my decodedPath(item -1 of xs))
        open location ("tinderbox://" & (item 2 of ys))
    end tell
end run

-- GENERIC
-- https://github.com/RobTrew/prelude-applescript

-- decodedPath :: Percent Encoded String -> FilePath
on decodedPath(fp)
    tell current application
        (its ((NSString's stringWithString:fp)'s ¬
            stringByRemovingPercentEncoding)) as string
    end tell
end decodedPath

-- splitOn :: String -> String -> [String]
on splitOn(pat, src)
    set {dlm, my text item delimiters} to ¬
        {my text item delimiters, pat}
    set xs to text items of src
    set my text item delimiters to dlm
    return xs
end splitOn
3 Likes

Those are great scripts, thanks a lot for putting them together @RobTrew

I’m going to add them to Hook as a built in integrations, but there are a few changes I’m making to the URL structure

  • removing that semicolon appended to the end of attribute “NoteURL”
  • changing the second ? to a &, to match query syntax

e.g. from
hook://tbx/file/note?view=outline+select=1575069337;?filepath=/path/file.tbx
to
hook://tbx/file/note?view=outline+select=1575069337&filepath=/path/file.tbx

I also changed the script to link to the file if no note is selected

2 Likes

The Tinderbox GUI Note > Copy Note URL uses that trailing semicolon to get an intercalating separator when several notes are selected:

e.g.

tinderbox://treeBook-003/?view=outline+select=1557888055;1558033595;1558033590;

Perhaps worth going beyond my first sketch to support that (multi-selection) case too ?

On a related note, we are working on Hook working with multi-selection in Finder for early 2020 (which was one of the early enhancement requests for Hook).

For example, updating both to enable multiple selections and being agnostic about use of either & or ? in the additional string (to avoid breaking any links that people have already made), perhaps sth like:

Get Address (and name)
use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

-- Rob Trew ver 0.2 
-- (Enabling restoration of multiple selections - also needs Open Item ver 0.2)

on run
    tell application "Tinderbox 8"
        set ds to documents
        if 0 < length of ds then
            set doc to item 1 of ds
            set oFile to file of doc
            
            if missing value is not oFile then
                set fp to my encodedPath(POSIX path of (oFile as alias)) as string
                
                set selns to selections of doc
                set intSelns to length of selns
                if 0 < intSelns then
                    set firstNote to item 1 of selns
                    if 1 < intSelns then
                        set strName to name of firstNote & " ETC"
                        script go
                            on |λ|(x)
                                value of attribute "ID" of x
                            end |λ|
                        end script
                        set strOtherIDs to ";" & my intercalate(";", my map(go, rest of selns))
                    else
                        set strName to name of firstNote
                        set strOtherIDs to ""
                    end if
                    set selnIDs to (value of (attribute "NoteURL" of firstNote)) as string
                    set strURL to (text (1 + (length of "tinderbox")) thru -2 of selnIDs)
                    "[" & strName & "](" & "tbx" & strURL & strOtherIDs & "&filepath=" & fp & ")"
                else
                    ""
                end if
            else
                "[Hook can't link an unsaved file]()"
            end if
        else
            ""
        end if
    end tell
end run

-- GENERIC 
-- https://github.com/RobTrew/prelude-applescript

-- encodedPath :: FilePath -> Percent Encoded String
on encodedPath(fp)
    tell current application
        (its ((NSString's stringWithString:fp)'s ¬
            stringByAddingPercentEncodingWithAllowedCharacters:(its NSCharacterSet's ¬
                URLPathAllowedCharacterSet))) as string
    end tell
end encodedPath

-- intercalate :: String -> [String] -> String
on intercalate(delim, xs)
    set {dlm, my text item delimiters} to ¬
        {my text item delimiters, delim}
    set str to xs as text
    set my text item delimiters to dlm
    str
end intercalate

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    -- The list obtained by applying f
    -- to each element of xs.
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn
Open Item
use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

on run
    set xs to splitOn("filePath=", "$0")
    set ys to splitOn("//", item 1 of xs)
    
    tell application "Tinderbox 8"
        activate
        open (my decodedPath(item -1 of xs))
        open location "tinderbox://" & (item 2 of ys)
    end tell
end run

-- GENERIC
-- https://github.com/RobTrew/prelude-applescript

-- decodedPath :: Percent Encoded String -> FilePath
on decodedPath(fp)
    tell current application
        (its ((NSString's stringWithString:fp)'s ¬
            stringByRemovingPercentEncoding)) as string
    end tell
end decodedPath

-- quoted :: Char -> String -> String
on quoted(c, s)
    -- string flanked on both sides
    -- by a specified quote character.
    c & s & c
end quoted

-- splitOn :: String -> String -> [String]
on splitOn(pat, src)
    set {dlm, my text item delimiters} to ¬
        {my text item delimiters, pat}
    set xs to text items of src
    set my text item delimiters to dlm
    return xs
end splitOn

As Luc mentioned, there are plans to support multi-selection in the near future. We’re going to hold off on supporting any kind of multi-selection in Tinderbox until we can do it in a way which is in line with Hook’s model for it.

Obviously users are welcome and encouraged to use custom scripts such as the ones you’ve written, but Tinderbox’s model of one URL to multiple selected notes is at odds with Hook’s (planned) model which is multiple URLs to multiple selected notes. So it won’t be included in the default integration.

After thinking about it I’ve decided to use the same URL structure as you initially designed, with identifier;?filepath for Hook’s default integration with Tinderbox

So the only functional changes to the scripts will be

  1. Returns a link to the file if no notes are selected
  2. Returns nothing if multiple notes are selected, anticipating future multi-selection support

These are the scripts that we plan to make the Hook default integration

Get Address

Aside from the functional changes mentioned above, this script has been flattened to fail first instead of using nested conditionals. This is an arbitrary stylistic preference.

use framework "Foundation"

tell application "Tinderbox 8"
	if (count of documents) is equal to 0 then
		return "No document is available"
	end if
	
	set openFile to file of first document
	if openFile is missing value then
		return "Hook can't link unsaved files"
	end if
	
	set filePath to my encodedPath(POSIX path of (openFile as alias)) as string
	
	set openNote to selected note of first document
	if openNote is missing value then
		-- link to file if no note is selected
		return "file://" & filePath
	end if
	
	set tinderboxURL to value of attribute "NoteURL" of openNote
	set tinderboxURL to (text (1 + (length of "tinderbox://")) thru -1 of tinderboxURL)
	set tinderboxURL to "hook://tbx/" & tinderboxURL
	
	return tinderboxURL & "?filepath=" & filePath
end tell

-- encodedPath :: FilePath -> Percent Encoded String
on encodedPath(fp)
	tell current application
		(its ((NSString's stringWithString:fp)'s ¬
			stringByAddingPercentEncodingWithAllowedCharacters:(its NSCharacterSet's ¬
				URLPathAllowedCharacterSet))) as string
	end tell
end encodedPath

Get Name

I personally favour splitting Get Name and Get Address apart, instead of returning a markdown link, unless there are performance reasons to do them both in one pass, e.g. if scripts rely on simulated menu items or button presses.

It is “safe” to directly access the selected note which may not exist because the only case this script needs to handle is if it does exist.

tell application "Tinderbox 8"
	get name of selected note of document 1
end tell
Open Item

I believe this is unchanged from the initial script posted by RobTrew

use framework "Foundation"
use scripting additions

set xs to splitOn("?filePath=", "$0")

set filePath to my decodedPath(item -1 of xs)
set tinderboxURL to text ((length of "tbx://") + 1) thru -1 of item 1 of xs
set tinderboxURL to "tinderbox://" & tinderboxURL

tell application "Tinderbox 8"
	activate
	open filePath
	open location tinderboxURL
end tell

-- GENERIC
-- https://github.com/RobTrew/prelude-applescript

-- decodedPath :: Percent Encoded String -> FilePath
on decodedPath(fp)
	tell current application
		(its ((NSString's stringWithString:fp)'s ¬
			stringByRemovingPercentEncoding)) as string
	end tell
end decodedPath

-- splitOn :: String -> String -> [String]
on splitOn(pat, src)
	set {dlm, my text item delimiters} to ¬
		{my text item delimiters, pat}
	set xs to text items of src
	set my text item delimiters to dlm
	return xs
end splitOn