Script: open everything in two windows

Use case

My workflow usually involves two document windows side-by-side. The window on the right is for my notes. The left is for reading and reference materials, i.e. everything else. Both windows may contain many tabs. I prefer this setup because I don’t need to switch between, or rearrange windows.

My setup relies on the two following functions:

  • When I open documents, they appear as tabs in the correct window: notes on the right, everything else on the left.
  • When I, for some reason, have many document windows open, I can reorganize them into two windows with ease.

The following script is capable of both.

Script (JXA)

Click to expand
/* This script is intended to maintain a work arrangement of 2 document windows.
   One type of documents open in window A; all other documents open in window B. You can change the criteria as you wish.
   If the front DT window is a viewer window, selected documents will be opened.
   If the front DT window is a document window, all document windows and their tabs will be rearranged.
   You might want to experiment with the script to see what it does. */

/* Meowky 2024-08-31 */

function run () {
	const app = Application('DEVONthink 3');
	app.includeStandardAdditions = true;
	
	const allDWs = app.documentWindows();
	
	// Determine which window a document should open in.
	function isNote (r) {
		// Change criteria as you wish. The example is for markdown notes.
		const criteria = r.type() === 'markdown' && r.database().name() === 'My Notes' && !r.name().startsWith('This is not a note!');
		
		if (criteria) return true;
		else return false;
	}
	
	// Designate two document windows; other documents will be (re)opened in these two.
	// Windows with the most open tabs will be prioritized.
	function autoSelectWindows (ws) {
		let readingWindow, notesWindow;
		
		// Loop through all document windows.
		for (let i = 0; i < ws.length; i++) {
			const w = ws[i];
			const test = w.tabs().map(x => isNote(x.contentRecord()));
			
			// Determine the "type" of a document window by comparing the number of open notes vs other document types.
			if (test.filter(x => x).length < test.filter(x => !x).length) {
				if (!readingWindow) readingWindow = w;
				else if (readingWindow.tabs().length < w.tabs().length) readingWindow = w;
			}
			else {
				if (!notesWindow) notesWindow = w;
				else if (notesWindow.tabs().length < w.tabs().length) notesWindow = w;
			}
		}
		
		// Return the two windows; values default to undefined.
		return {'reading': readingWindow, 'notes': notesWindow};
	}
	
	const windows = autoSelectWindows(allDWs);
	
	if (app.thinkWindows()[0].class() === 'viewerWindow' && app.selectedRecords()) {
		
		// Front window is viewer window (that is, main window). Open selected records.
		app.selectedRecords().forEach(record => {
		
			// Ignore selected groups.
			if (record.type() === 'group') return;
			
			if (isNote(record)) {
				if (windows.notes) {
					app.openTabFor({record: record, in: windows.notes});
				} else {
					// Create a new document window for notes if there is no designated window for notes.
					windows.notes = app.openTabFor({record: record}).thinkWindow();
				}
			} else {
				if (windows.reading) {
					app.openTabFor({record: record, in: windows.reading});
				} else {
					windows.reading = app.openTabFor({record: record}).thinkWindow();
				}
			}
		})
	} else {
		
		// Front window is document window. Reorganize document windows.
		// Garner all open tabs in all document windows. NOTE: tabs in viewer windows are untouched.
		const allTabs = allDWs.map(x => x.tabs()).flat();
		
		// Loop through the tabs.
		allTabs.forEach(tab => {
			const record = tab.contentRecord();
			if (isNote(record)) {
				if (!windows.notes) {
					// Create new window for notes if there is no designated window.
					app.close(tab, {saving: 'ask'});
					windows.notes = app.openTabFor({record: record}).thinkWindow();
				}
				else if (tab.thinkWindow().id() !== windows.notes.id()) {
					// Move the tab if a note is not opened in the designated window for notes.
					app.close(tab, {saving: 'ask'});
					app.openTabFor({record: record, in: windows.notes});
				}
			} else {
				if (!windows.reading) {
					// Create new window ...
					app.close(tab, {saving: 'ask'});
					windows.reading = app.openTabFor({record: record}).thinkWindow();
				}
				else if (tab.thinkWindow().id() !== windows.reading.id()) {
					// Move ...
					app.close(tab, {saving: 'ask'});
					app.openTabFor({record: record, in: windows.reading});
				}
			}
		})
	}
	
}

Some additional comments:

  • You will need to modify the criteria to fit your use case. Start with changing 'My Notes' to the name of your database.
  • This script manipulates the UI only, not the data. Its exact behavior depends on the class of the front DT window.
  • This script is quite different from the Open In Two Windows script shipped along with the software. More specifically, my script can handle more than two documents, and runs with or without selected documents. The other script serves an entirely different purpose.
  • Want to run the script with a keyboard shortcut? In case you don’t know yet, you can specify the keyboard shortcut in the filename of the script, for example: MyScript___Shift+Cmd+Option+Control+U.scpt
    There are alternative means as well.

Feedback is welcome!

5 Likes

Have you checked out this extra script?

And the description is actually only one use case. Install it, select two documents, and run the script.

1 Like

Here it comes. All, of course, my personal opinion.

  • The code in isNote() is a bit too verbose. A simple
    return r.type() === 'markdown' && r.database().name() === 'My Notes' && !r.name().startsWith('This is not a note!);
    suffices.
  • Instead of for (let i = ...), I’d use
    ws.forEach(w => { const test w.tabs()...}...)
  • I think that w.tabs().map(x => isNote(x.contentRecord())) could be written simpler as w.tabs.contentRecord().map(x => isNote(x))
  • Instead of if (!readingWindow) ... else if (...) , I’d use an or condition and a single if:
    if (!readingWindow or readingWindow.tabs().length < w.tabs().length) readingWindow = w
  • Same for the next if … else if condition.
  • I’m still struggling with the test.filter(x => x).length ... condition. If I understand the code correctly, test is an Array of Boolean, where each array element tells us if records in the current window’s tabs are notes or not. The test condition seems to figure out if you have more notes than non-notes in the current window’s tabs. I’d probably use something like
const noteCount = w.tabs.contentRecord().filter(x => isNote(x)).length;
if (noteCount < w.tabs.length - noteCount) {
  if (!readingWindow …) {
  }
else {
  if (!notesWindow) {
}
}

I may of course have gotten the idea wrong or the conditions upside down. But the current code seems to be a bit over-complicated – at least if all you want to know is how many notes you have in the current window. If that’s the case, I’d use a filter, not a map on the on the contentRecords

  • Instead of app.selectedRecords().forEach(record => { if record.type === 'group' return; … }), I’d use
    app.selectedRecords().filter(r => r.type !== 'group').forEach(r => …)
    The rest of the code is way over my head. It looks as if it might be simplified, but I have no clue how.
2 Likes

I actually addressed that script in my original post. It serves a different purpose and, understandably, fulfills neither of my two needs.

Many thanks for the coding lesson! Some of the tips I’m previously unaware of.

I must admit that I’m quite new to JXA and JavaScript in general, having recently switched from AppleScript. I was focused on making this script work; making it more elegant was of secondary concern unless there is a significant speed penalty.

I will perhaps rewrite the script when I want to add new functions into it. Thanks again!

can you please show a screenshot of your 2-window setup, so that I can better understand how you use DTP. thanks.

As described in the original post, I have two document windows open in the front. On the secondary screen there are another one or two viewer windows (not shown in screenshot) for searching and organizing.

can you share the full scrip pls

Copy the script text (available in the “Click to expand” section), then paste it into Script Editor. Be assured that it is the full script :slight_smile:

1 Like

I doubt that this script would exhibit any performance problems, even if coded in a worse way. But “elegance” is also about clarity, i.e. making the code easier to understand and consequently easier to maintain. Which is important for many people – more so than performance considerations.

In particular, your approach to getting the number of notes in a window is quite winded and obscures the intention (in my opinion, of course). I’d ask myself if I will understand what I did there a week or two later. Been there, done that :wink:

2 Likes