It’s an sqlite file, not too difficult to script - tho the shelf-life of such a script would clearly be finite.
Of the two experiments which they have run:
- A unified approach to making labelled links to local resources,
- showing the user only links which have been made to or from the active (front window) resource.
Experiment 1 has succeeded (produced some real value), but experiment 2 has failed (produced more frustration and distraction than value), and as the database is structured around experiment 2, its schema seems very likely to be changed.
Given that caveat, its probably OK to post something like this, which just lists a menu of all the links created so far, and let us make a selection,
(tho Luc has expressed understandable uneasiness about users getting direct access to the sqlite3 store of their links).
JavaScript for Automation
- Test in Script Editor with top-left language tab set to JavaScript, or in a Keyboard Maestro Execute JavaScript action, etc
- after selecting and copying the whole source, scrolling all the way down to
// MAIN --- return main(); })();
(() => {
'use strict';
ObjC.import('sqlite3');
// HOOK: SIMPLE EXAMPLE OF TABLE-READING FROM JAVASCRIPT FOR AUTOMATION
// Rob Trew 2019
const main = () => {
const multipleSelections = true;
return sj(either(
msg => msg,
xs => {
const
links = nubBy(
a => b => snd(a) === snd(b),
xs
),
addrs = map(fst, links),
labels = map(snd, links),
sa = standardSEAdditions();
return bindLR(
showMenuLR(true, 'Hook links', labels),
choices => map(
x => {
const
strAddr = addrs[elemIndex(x, labels).Just],
strURL = strAddr.startsWith('/') ? (
'file://' + strAddr + '/' + x
) : strAddr
return (
sa.activate(),
sa.openLocation(strURL),
strURL
);
},
choices
)
);
},
linksFromHooKDBPathLR(
'~/Library/Application Support/' +
'com.cogsciapps.hook/hook.sqlite'
)
));
};
// HOOK.APP - SIMPLEST LISTING OF LINKS VIA SQLITE
// linkAndLabelFromMeta :: String -> [String]
const linkAndLabelFromMeta = s => {
const xs = s.split('$$$');
return 1 < xs.length ? (
[xs[0], base64decode(xs[1])]
) : [s, ''];
};
// linksFromHooKDBPathLR :: FilePath -> Either String [String]
const linksFromHooKDBPathLR = strDBPath => {
const
SQLITE_OK = parseInt($.SQLITE_OK, 10),
SQLITE_ROW = parseInt($.SQLITE_ROW, 10),
ppDb = Ref(),
strSQL =
'SELECT srcMetaString, destMetaString, ' +
"COALESCE(path, '') as folder, " +
'COALESCE(name, "") as fileName ' +
'FROM link l LEFT JOIN fileinfo f ' +
'ON l.dest=f.fileid ' +
'ORDER by src',
colText = curry($.sqlite3_column_text);
return bindLR(
bindLR(
SQLITE_OK !== $.sqlite3_open(filePath(strDBPath), ppDb) ? (
Left($.sqlite3_errmsg(fst(ppDb)))
) : Right(fst(ppDb)),
db => {
const ppStmt = Ref();
return SQLITE_OK !== $.sqlite3_prepare_v2(
db, strSQL, -1, ppStmt, Ref()
) ? (
Left($.sqlite3_errmsg(db))
) : Right(Tuple3(
db,
fst(ppStmt),
enumFromTo(
0,
$.sqlite3_column_count(ppStmt[0]) - 1
)
));
}
),
// (Link, labe) from all available rows in the table:
tpl => Right(
sortBy(mappendComparing([snd]),
concatMap(
x => {
const [from, to] = map(
linkAndLabelFromMeta,
x.slice(0, 1)
).concat([x.slice(2)]);
return (0 < (
fst(to).length + snd(to).length)) ? (
[from, to]
) : [from];
},
unfoldr(
stmt => SQLITE_ROW !== $.sqlite3_step(stmt) ? (
$.sqlite3_finalize(stmt),
$.sqlite3_close(fst(tpl)),
Nothing()
) : Just(
Tuple(
map(colText(stmt), tpl[2]),
stmt
)
),
snd(tpl)
)
)
)
)
);
};
// JXA ------------------------------------------------
// base64decode :: String -> String
const base64decode = s =>
ObjC.unwrap(
$.NSString.alloc.initWithDataEncoding(
$.NSData.alloc.initWithBase64EncodedStringOptions(
s, 0
),
$.NSUTF8StringEncoding
)
);
// showMenuLR :: Bool -> String -> [String] -> Either String [String]
const showMenuLR = (blnMult, title, xs) =>
0 < xs.length ? (() => {
const sa = standardSEAdditions();
sa.activate();
const v = sa.chooseFromList(xs, {
withTitle: title,
withPrompt: 'Select' + (
blnMult ? ' one or more of ' +
xs.length.toString() : ':'
),
defaultItems: xs[0],
okButtonName: 'OK',
cancelButtonName: 'Cancel',
multipleSelectionsAllowed: blnMult,
emptySelectionAllowed: false
});
return Array.isArray(v) ? (
Right(v)
) : Left('User cancelled ' + title + ' menu.');
})() : Left(title + ': No items to choose from.');
// standardSEAdditions :: () -> Application
const standardSEAdditions = () =>
Object.assign(Application('System Events'), {
includeStandardAdditions: true
});
// 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
});
// Tuple (,) :: a -> b -> (a, b)
const Tuple = (a, b) => ({
type: 'Tuple',
'0': a,
'1': b,
length: 2
});
// Tuple3 (,,) :: a -> b -> c -> (a, b, c)
const Tuple3 = (a, b, c) => ({
type: 'Tuple3',
'0': a,
'1': b,
'2': c,
length: 3
});
// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
const bindLR = (m, mf) =>
undefined !== m.Left ? (
m
) : mf(m.Right);
// compare :: a -> a -> Ordering
const compare = (a, b) =>
a < b ? -1 : (a > b ? 1 : 0);
// comparing :: (a -> b) -> (a -> a -> Ordering)
const comparing = f =>
(x, y) => {
const
a = f(x),
b = f(y);
return a < b ? -1 : (a > b ? 1 : 0);
};
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) =>
xs.flatMap(f);
// curry :: ((a, b) -> c) -> a -> b -> c
const curry = f => a => b => f(a, b);
// 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;
// elemIndex :: Eq a => a -> [a] -> Maybe Int
const elemIndex = (x, xs) => {
const i = xs.indexOf(x);
return -1 === i ? (
Nothing()
) : Just(i);
};
// enumFromTo :: Int -> Int -> [Int]
const enumFromTo = (m, n) =>
Array.from({
length: 1 + n - m
}, (_, i) => m + i);
// filePath :: String -> FilePath
const filePath = s =>
ObjC.unwrap(ObjC.wrap(s)
.stringByStandardizingPath);
// fst :: (a, b) -> a
const fst = tpl => tpl[0];
// identity :: a -> a
const identity = x => x;
// intercalate :: String -> [String] -> String
const intercalate = s => xs =>
xs.join(s);
// mappendComparing :: [(a -> b)] -> (a -> a -> Ordering)
const mappendComparing = fs =>
(x, y) => fs.reduce(
(ordr, f) => (ordr || compare(f(x), f(y))),
0
);
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) =>
(Array.isArray(xs) ? (
xs
) : xs.split('')).map(f);
// nubBy :: (a -> a -> Bool) -> [a] -> [a]
const nubBy = (fEq, xs) => {
const go = xs => 0 < xs.length ? (() => {
const x = xs[0];
return [x].concat(
go(xs.slice(1)
.filter(y => !fEq(x)(y))
)
)
})() : [];
return go(xs);
};
// showJSON :: a -> String
const sj = x => JSON.stringify(x, null, 2);
// showLog :: a -> IO ()
const showLog = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// snd :: (a, b) -> b
const snd = tpl => tpl[1];
// sortBy :: (a -> a -> Ordering) -> [a] -> [a]
const sortBy = (f, xs) =>
xs.slice()
.sort(f);
// unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
const unfoldr = (f, v) => {
let
xr = [v, v],
xs = [];
while (true) {
const mb = f(xr[1]);
if (mb.Nothing) {
return xs
} else {
xr = mb.Just;
xs.push(xr[0])
}
}
};
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// MAIN ---
return main();
})();