Skip to content

Commit

Permalink
Added group search, amended output
Browse files Browse the repository at this point in the history
  • Loading branch information
chrillek committed Nov 2, 2022
1 parent 8311678 commit a45892c
Show file tree
Hide file tree
Showing 6 changed files with 386 additions and 217 deletions.
10 changes: 9 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,13 @@
"markdown",
"latex",
"plaintext"
],
"runOnSave.commands": [
{
"match": ".(js|png)$",
"command": "cp ${file} '/Users/ck/Library/Mobile Documents/com~apple~CloudDocs/Alfred/Alfred.alfredpreferences/workflows/user.workflow.F83FA9C7-6984-40E7-9397-43B755FC2BF7'",
"runIn": "terminal",
"finishStatusMesssage": "${file} copied to Alfred"
}
]
}
}
209 changes: 153 additions & 56 deletions DT-Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ ObjC.import("Foundation");

const cmdMap = {
list_databases: listDatabases /* DTD, select_DB */,
list_groups: listGroups /* new command */,
list_smartgroups: listSmartgroups /* DTSG, list_all_smartgroups */,
list_tags: listTags /* DTT, list_all_tags */,
list_favorites: listFavorites /* DTF, list_favorites */,
Expand All @@ -18,20 +19,31 @@ const cmdMap = {
searchInDT /* search_with_DT_URL, search_in_new_window. This runs the query in DT and populates the result window there */,
load_workspace: loadWorkspace /* load_workspace */,
save_workspace: saveWorkspace /* save_workspace */,
list_searches: listSearches /* list the last 10 searches (should the no. be configurable?) */,
list_searches:
listSearches /* list the last 10 searches (should the no. be configurable?) */,
};

const app = Application("DEVONthink 3");
const searchFile = 'searches.json';
const searchFile = "searches.json";
const error = $();
const savedSearches = loadSearches();

/* get curApp object for shell commands */
const curApp = Application.currentApplication();
curApp.includeStandardAdditions = true;

function alfabetic_sort(a, b, field) {
const af = a[field]();
const bf = b[field]();
return af > bf ? 1 : af < bf ? -1 : 0;
}

function loadSearches() {
const string = $.NSString.stringWithContentsOfFileEncodingError($(searchFile),$.NSUTF8StringEncoding, error);
const string = $.NSString.stringWithContentsOfFileEncodingError(
$(searchFile),
$.NSUTF8StringEncoding,
error
);
if (string && string.js) {
return JSON.parse(string.js);
} else {
Expand All @@ -40,9 +52,14 @@ function loadSearches() {
}

function saveSearches() {
// console.log(`Saving ...${savedSearches}`);
// console.log(`Saving ...${savedSearches}`);
const string = $(JSON.stringify(savedSearches));
string.writeToFileAtomicallyEncodingError($(searchFile), false, $.NSUTF8StringEncoding, error);
string.writeToFileAtomicallyEncodingError(
$(searchFile),
false,
$.NSUTF8StringEncoding,
error
);
}

/* Get an environment variable and return its value as JS string or undefined if it doesn't exist */
Expand All @@ -62,26 +79,40 @@ function getIgnoredDBs() {
: [];
}

/* Return the DB defined in the preceding action. If 'all' is true, return an array containing all databases,
/* Return the DB defined in the preceding action.
If 'all' is true, return an array containing all databases,
excluding those in 'ignoredDbUuidList' */
function getDB(all) {
const DB = getEnv("selectedDbUUID");
// console.log(`DB ${DB}`);
console.log(`DB ${DB}`);
if (all) {
const ignoredDB = getIgnoredDBs();
return (DB ? [app.getDatabaseWithUuid(DB)] : [...app.databases()]).filter(
(uuid) => !ignoredDB.includes(uuid)
);
} else {
return app.getDatabaseWithUuid(DB);
return DB ? app.getDatabaseWithUuid(DB) : undefined;
}
}

/* Build tag string for subtitles */
function tagString(tags) {
function formatTags(r) {
const tags = r.tags();
return tags.length > 0 ? tags.join(", ") : "no tags";
}

/* Format the location for a record.
Returns 'database > group 1 > group 2 > ...
*/
function formatLocation(r) {
const location = r
.location()
.replaceAll(/^\/|\/$/g, "")
.replaceAll(/\//g, "#")
.replaceAll(/\/#/g, "/");
return [r.database.name(), ...location.split("#")].join(" > ");
}

/* check if the argument is an array with at least one empty element */
function checkArg(arg) {
const caller = arguments.callee.caller.name;
Expand All @@ -99,11 +130,12 @@ function checkArg(arg) {

function run(arg) {
const cmd = checkArg(arg);
// console.log(`command ${cmd}`);
/* Basic error checking */
if (!cmd) return `{items: [Title: 'No command!']}`;
const cmdHandler = cmdMap[cmd];
if (!cmdHandler) return `{items: [Title: 'No cmdHandler for "${cmd}"!']}`;
/* Call the command handler with the remaining arguments */
/* Call the command handler with the remaining argument(s) */
const result = cmdHandler(arg.slice(1));
return result ? JSON.stringify({ items: result }) : undefined;
}
Expand Down Expand Up @@ -138,49 +170,59 @@ function searchForAlfred(arg) {
.filter((t) => !/^$/.test(t))
.map((t) => `tags: ${t} `)
.join("");
console.log(query);
// console.log(query);
// curApp.displayAlert(`"${query}"`);
}
const databases = getDB(true); /* Array with all databases to search */
const databases = getDB(true); /* Array with databases to search */
const searchGroup = getEnv("searchGroup");
const resultArray = [{ query: query }];
savedSearches.push({q: query, db: (databases.length > 1 ? undefined : databases[0])});
savedSearches.push({
q: query,
db: databases.length > 1 ? undefined : databases[0],
group: searchGroup || undefined
});
if (savedSearches.length > 10) {
savedSearches.shift();
}
saveSearches();

databases.forEach(db => {
databases.forEach((db) => {
// search in record corresponding to the database
const resultList = app.search(query, { in: db.root() });

resultList.forEach(record => {
const uuid = record.uuid();
const location = (() => {
const loc = record.location();
if (loc.length > 1) {
return loc.slice(0, -1).replace(/\//g, " > ");
} else {
return "";
}
})();

const tagStr = tagString(record.tags());
const dtLink = `x-devonthink-item://${uuid}`;
const name = record.name();
const path = record.path();
resultList.forEach(r => console.log(r.locationGroup.name()));
resultList.filter(r =>
searchGroup ? r.locationGroup.name() === searchGroup : true).forEach(r => {
const uuid = r.uuid();
const location = formatLocation(r);
const tagStr = formatTags(r);
const dtLink = r.referenceURL();
const name = r.name();
const isGroup = r.type() === "group";
const path = isGroup
? dtLink
: r.path(); /* set to path for normal records and to dtLink otherwise */
/* Use defaut DT's group icon for groups, thumbnail for normal records */
const icon = isGroup
? { path: `./group.png` }
: { type: "fileicon", path: path };
resultArray.push({
type: "file:skipcheck",
title: name,
score: record.score(),
score: r.score(),
tags: tagStr,
arg: path /* To open in default editor */,
subtitle: `📂 ${db.name()}: ${location}`,
icon: { type: "fileicon", path: path },
subtitle: `📂 ${location}`,
icon: icon,
mods: {
cmd: { valid: true, arg: uuid, subtitle: `🏷 ${tagStr}` },
alt: { valid: true, arg: uuid, subtitle: "Reveal in DEVONthink" },
cmd: {
arg: uuid,
subtitle: `🏷 ${tagStr}`,
},
alt: {
arg: r.referenceURL(),
subtitle: "Reveal in DEVONthink",
},
shift: {
valid: true,
arg: `[${name}](${dtLink})`,
subtitle: "Copy Markdown Link",
},
Expand All @@ -199,9 +241,8 @@ function searchForAlfred(arg) {

function listDatabases() {
/* get an array of all databases sorted by name */
const DBs = app
.databases()
.sort((a, b) => (a.name() > b.name() ? 1 : a.name() < b.name() ? -1 : 0));
const DBs = app.databases().sort((a, b) => alfabetic_sort(a, b, "name"));

const items = DBs.map((db) => {
return {
title: db.name(),
Expand All @@ -216,7 +257,10 @@ function listDatabases() {
/* Open record in DT, using the UUID passed in arg[0] */
function openRecord(arg) {
const uuid = checkArg(arg);
app.openWindowFor({ record: app.getRecordWithUuid(uuid) });
app.openWindowFor({
record: app.getRecordWithUuid(uuid),
force: getEnv("alwaysOpenWindow"),
});
}

/* Find all records with the tag in env('selectedTag'). Uses searchForAlfred. */
Expand All @@ -240,7 +284,7 @@ function listSmartgroups() {
smartGroupList.forEach((sg) => {
items.push({
title: sg.name(),
subtitle: `📂 ${db.name()} (${sg.children().length} elements)`,
subtitle: `📂 ${db.name()} (${sg.children().length} items)`,
arg: sg.uuid(), // pass on smart groups UUID to next action, i.e. open in DT
});
});
Expand All @@ -252,14 +296,19 @@ function listSmartgroups() {
function searchInDT(arg) {
const query = checkArg(arg).replace(/(\p{sc=Han}+)/gu, " ~\1 ");
if (arg.length === 1) {
/* there is an additional argument passed in - forward it to DT using its url scheme */
curApp.doShellScript(
`open 'x-devonthink://search?query=${encodeURIComponent(query)}'`
);
} else {
/* Only query passed in. Use DT's top window to run the search */
app.activate();
const r = app.inbox.root();
const tw = app.openWindowFor({ record: r }, { force: true });
tw.searchQuery = query;
const window = app.openWindowFor(
{ record: r },
{ force: getEnv("alwayOpenWindow") }
);
window.searchQuery = query;
}
}

Expand All @@ -275,19 +324,24 @@ function listFavorites() {
const v = it.value();
if ("UUID" in v) {
/* Normal record */
return { title: `📁 v.Name`, arg: v.UUID};
const path = app.getRecordWithUuid(v.UUID).path();
const obj = { title: `${v.Name}`, arg: v.UUID };
if (path) {
obj.quicklookurl = `file://${path}`;
} else {
obj.icon = { path: "./group.png" };
}
return obj;
} else if ("Path" in v) {
/* Database */
const uuid = (() => {
/* Database. Open it to get its UUID */
const uuid = (() => {
const db = app.openDatabase(v.Path);
return db.uuid();
})();
return { title: v.Name, arg: uuid };
return { title: v.Name, arg: uuid, icon: { path: "./database.png" } };
}
});
return items.length
? items
: [{ title: "No favorite items" }];
return items.length ? items : [{ title: "No favorite items" }];
}

function listWorkspaces() {
Expand All @@ -310,10 +364,53 @@ function saveWorkspace(argv) {

function listSearches() {
loadSearches();
const items = savedSearches.map(s => {
const dbString = s.db ||'All databases';
const scopeString = (s.db ? ` scope:${s.db}`: '');
return {title: `${s.q} (${dbString})`, arg: `${s.q}${scopeString}`};
})
const items = savedSearches.map((s) => {
const dbString = s.db || "All databases";
const scopeString = s.db ? ` scope:${s.db}` : "";
return { title: `${s.q} (${dbString})`, arg: `${s.q}${scopeString}` };
});
return items.length ? items : [{ title: "No queries saved" }];
}
}

function listGroups() {
const db = getDB(false);
if (db) {
const tagGroups = db.tagGroups.uuid();
tagGroups.push(db.tagsGroup.uuid());
tagGroups.push(db.trashGroup.uuid());
const groups = db.parents
.whose({
_and: [
{ _match: [ObjectSpecifier().type, "group"] },
{ _not: [{ _match: [ObjectSpecifier().type, "tag"] }] },
],
})()
.filter((g) => !tagGroups.includes(g.uuid()));
const items = groups
.sort((a, b) => alfabetic_sort(a, b, "location"))
.map((g) => {
return {
arg: g.referenceURL(),
title: `'${g.name()}' in ${formatLocation(g)}`,
subtitle: `Open in DT`,
type: "file:skipcheck",
mods: {
alt: {
arg: g.referenceURL(),
subtitle: "Reveal in DEVONthink",
} /* pass only the uuid here since the Alfred action builds the item link itself - USEFUL? */,
cmd: {
arg: "",
variables: {
searchGroup: g.name(), /* Name is needed for search scope later */
selectedDbUUID: db.uuid(),
},
},
},
};
});
return items.length ? items : [{ title: "No groups found" }];
} else {
return [{ title: "No database selected" }];
}
}
Binary file added database.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added group.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit a45892c

Please sign in to comment.