I hope I can explain it in a way that’s understandable.
The plan is to improve the naming of my Markdown files for better linking. But I still want it to be a bit Human Readable.
An example would be YYYY-MM-DD-SHORT name.md
With the following I struggle:
SHORT: That should be based on the database, but not the exact database name.
Example:
I have the database “Projects 2025” and the SHORT should be PJ25 or even just PJ since the date is in the name anyway.
But where can I define that in a way to access it again in the rule to change the name?
I can’t use the custom data types since I want to archive when I move the file from the Inbox, or create the file outside the Inbox.
Which means
The rule should apply in two cases.
When the document is moved away from the inbox to the DB (simple)
When a markdown is created anywhere other than the Inbox
I would love to filter that somehow, since I guess the alternative would be a rule for every Database?
So in short, my two Problems:
Placehold Based on Database Name for File Rename
Rule running when moving from Inbox or only when creating the file outside of Inbox.
You rename your databases to the short name so that the Database Name placeholder could be used.
An input/output script that maps the database name to the short name:
on scriptOutput(theRecord, theInput)
tell application id "DNtp"
if theInput is "Projects 2025" then return "PJ25"
return theInput
end tell
end scriptOutput
But what about the creation? I want this to run in any database, but not the Inbox. So only when I move the document from there (or somewhere else) the rule should start.
Then you must attach it to the database you want to use it with: Replace Suchen in Datenbanken with Suchen in <name of database>. There’s no Search everywhere _but_ operator.
Are you intending to move documents from database to database?
So only when I move the document from there (or somewhere else) the rule should start.
Again, rules aren’t triggered when moving things out of a database. They only get triggered when receiving something, either by creation, moving, importing, classifying, etc.
PS: A simple but more error-trapped script…
on scriptOutput(theRecord, theInput)
tell application id "DNtp"
set db to comment of (database of theRecord)
if db is not "" then
return comment of (database of theRecord)
else
log message "Database has no comment on it."
end if
end tell
end scriptOutput
How many databases do you use?
If you have many databases, another approach could be to use a single “Move & Rename” script instead of smart rules. Something like:
tell application id "DNtp"
if selected records = {} then return
set theDestination to display group selector
set db to comment of (database of theDestination)
repeat with theRecord in selected records
set dateStr to short date string of (get creation date of theRecord)
set oldName to name without extension of theRecord
set newName to dateStr & "-" & db & " " & oldName
set name of theRecord to newName
move record theRecord to theDestination
end repeat
end tell
This wouldn’t run automagically in the background, of course… What approach you find more appealing depends on setup and preference. (I’ll add a reminder that you can put scripts in the toolbar and/or give them keyboard shortcuts.)
Likewise, you can use a script for creating notes that follow a consistent naming scheme:
tell application id "DNtp"
set theLocation to current group
set db to comment of (database of theLocation)
set dateStr to short date string of (current date)
set theTitle to display name editor info "Note title:"
set theName to dateStr & "-" & db & " " & theTitle
set newNote to create record with {name:theName, record type:markdown, plain text:("# " & theTitle & linefeed & linefeed)} in theLocation
set current tab of think window 1 to (open tab for record newNote in think window 1)
end tell
Here is a “Move & Rename” in JavaScript, which makes it much easier to use regular expressions. Benefits:
It works for documents that already follow your naming scheme – only “SHORT” is changed when moving between databases.
If the name starts with a date string already, it uses that. Useful if for certain documents you want to use a different date than the creation date.
When a document is moved to a destination in the same database, it is not renamed. Meaning: if you select a bunch of documents from different databases via search or a smart group, only the documents that change database are renamed.
/* Move & Rename markdown documents *
...following the scheme:
YYYY-MM-DD-<Database Shortname> <Name>
- <Database Shortname> is stored in Comments field of Database
Properties.
- Existing date string is used if present, otherwise creation
date is used.
*/
(() => {
const app = Application('DEVONthink');
app.includeStandardAdditions = true;
const records = app.selectedRecords().whose(
{_match: [ObjectSpecifier().recordType, 'markdown'] }
)();
if (records.length === 0) {
app.displayAlert('No markdown documents selected');
return;
}
const destination = app.displayGroupSelector();
const db = destination.database.comment();
records.forEach(r => {
if (destination.database.id() !== r.database.id()) {
const nameParts = r.nameWithoutExtension().match(
/^(?:(\d{4}-\d{2}-\d{2})(?:-\S+)?)?(.+)$/);
let dateStr;
if (nameParts[1]) {
dateStr = nameParts[1];
} else {
dateStr = r.creationDate().toISOString().slice(0,10);
}
r.name = `${dateStr}-${db} ${nameParts[2]}`;
}
app.move({record: r, to: destination});
});
})()
Edit:
Seems like destination.database() !== r.database() will always return true. You have to compare something more specific like database.id() or database.name().
@bkuhn You’re welcome
(You didn’t say how many databases you use?)
Theoretically, I guess you could search everywhere and then filter records further in a smart rule script? But I’m not sure how performant that is and whether it’s really a good idea. I haven’t tried.
Something like:
function performsmartrule(records) {
var app = Application("DEVONthink");
app.includeStandardAdditions = true;
records.forEach (r => {
if (r.database.id() !== 1) {
// Do stuff
}
})
}
(I think the id of the global inbox is always 1? Otherwise you could use the name.)
Does that seem like a bad idea?
A more fleshed out example – not tested:
function performsmartrule(records) {
var app = Application("DEVONthink");
app.includeStandardAdditions = true;
records.forEach (r => {
if (r.recordType() === "markdown" // Extra check for redundancy
&& r.database.id() !== 1 ) {
const db = r.database.comment();
if (db === "") {app.logMessage(
`Skipped – database "${r.database.name()}" has no comment on it.`,
{record: r});
} else if (db.length > 10) {app.logMessage(
`Skipped – comment of database "${r.database.name()}" is too long.`,
{record: r});
} else {
const nameParts = r.nameWithoutExtension().match(
/^(?:(\d{4}-\d{2}-\d{2})(?:-\S+)?)?(.+)$/);
let dateStr;
if (nameParts[1]) {
dateStr = nameParts[1];
} else {
dateStr = r.creationDate().toISOString().slice(0,10);
}
r.name = `${dateStr}-${db} ${nameParts[2]}`;
}
}
})
}
A dream would be a real Python implementation, with real Python style and not just on top of Apple Script. A few elegant lines would solve my problem. And would look way more readable as JS or AppleScript anyway.
There are Python-OSA bridges. I have no idea how well they’re implemented, though. And some time ago, someone posted a link to a Python API for DT. That was a wrapper around JXA, though.
If code appears legible or not depends, imo, on how it is written and what the reader is used to. Personally, I cringe when indentation becomes syntax. But I grew up with (or instead of) Fortran.
Good feedback, I appreciate it. I used the default template and didn’t stop to think about that. Like this?
function performsmartrule(records) {
var app = Application("DEVONthink");
app.includeStandardAdditions = true;
records.filter(r =>
r.recordType() === "markdown" // Extra check for redundancy
&& r.database.id() !== 1
).forEach(r => {
// Do stuff
})
}
whose actually does work for filtering on database id. The question is if you can even use whose in a smart rule script to begin with. Or more precisely, if you can use it on the records parameter of the performsmartrule function…
I don’t think that’s possible? From what I understand whose always has be a direct call to the application – you can’t use it to filter arbitrary lists/arrays.
But I can’t make whose work in a smart rule. I just get “Error: TypeError: records.whose is not a function.”
Thank you for the compliment! I’ll gladly explain for the class
(?: ) is a non-capturing group. You use it when you want to check for a specific pattern without capturing anything.
? is a quantifier, shorthand for “0 or 1 of” – making what the quantifier applies to optional.
The trick is: you can put a capture group inside a non-capturing group to catch a specific part of a larger pattern… And then put a ? after the outer non-capturing group. That way you only capture something if the outer non-capturing group is matched—and the overall expression works whether you capture anything or not.
(The ? quantifier also works for capture groups that aren’t nested in non-capturing ones.)
While I’m at it, let me give the full rundown:
We are looking for the filename pattern YYYY-MM-DD-<Database Shortname> <Name>.
We want to capture the date string if it’s there, but not -<Shortname>. We also want to capture <Name>.
Here is the RegEx again:
^(?:(\d{4}-\d{2}-\d{2})(?:-\S+)?)?(.+)$
^ and $ are anchor points, referring to the start and end of the input.
(With the m flag, they instead refer to the start and end of a line.)
After ^ comes a non-capturing group, testing for…
… the date string: (\d{4}-\d{2}-\d{2}) (capture group 1)
… the Shortname: (?:-\S+)?, using another non-capturing group.
- is a literal dash.
\S means any non-whitespace character.
+ is a quantifier, shorthand for “1 or more of”.
(Potentially too greedy, we could instead specify a range like {2,10})
After the group we add a ?, making the Shortname optional.
We also put a ? after the outer non-capturing group, since the filename might not have a date string yet.
At the end we have capture group 2, catching everything else – the Name: (.+)$
. matches anything
+ one or more times.
Until the end, $.
I actually missed an \s? (optional whitespace) just before capture group 2, which could lead to a double-space in the renamed name. But DEVONthink automatically fixes double-space in filenames.
whose works on “Elements”. That’s what script editor calls them, e.g. contents, parents, smartParents etc. for database. Those are object specifiers which () “dereferences” (for lack of a better term). whose works on the object specifier, not on the dereferenced object. The same goes for selectedRecords: it is an object specifier, dereferencing converts it to an Array on which you can call all array methods.
And the records parameter of performsmartrule is not an object specifier, but an Array (or a list in AppleScript). Therefore, you can use all Array methods on it (filter, forEach etc), but not whose.
The advantage of whose is that it is blazingly fast. If that is relevant for the cases where one uses smart rules in DT? I don’t know.
Thanks for the detailed breakdown. I asked for it because at first I’d assumed that the RE would fail if there’s no date. Which I tried, and saw that it worked. And that got me thinking. Having the RE match even if there’s no date and having nameParts[1] empty but nameParts[2] defined – that’s cool.