Notes

Automating Notes

Notes is another of Apple’s applications with half-baked scripting support. There are about a gazillion questions about weird behavior and errors…

So there are only two examples in this section. The first one shows how to save all selected notes as HTML and plain text in a pre-defined folder, the second how to copy the attachments of the selected notes to a pre-defined folder. It should be fairly easy to combine both of them into one that saves the text and attachments in the same folder.

Save notes as HTML and plain text #

The script goes over all notes and saves their content as plain text and HTML in a subfolder of the predefined baseFolder. The name of this subfolder is derived by the note’s name, with special characters replaced by underscores.

Note that the text is not written with the JXA method write() because this one does not allow to define Unicode encoding. Without that, using write() would break any text outside the ASCII code range. So the script uses a part of the JXA-ObjectiveC-Bridge, described here.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
ObjC.import("Foundation");

(() => {
  const curApp = Application.currentApplication();
  curApp.includeStandardAdditions = true;
  const baseFolder = `${curApp.pathTo("desktop")}/Notes`;

  const notesApp = Application("Notes");
  const notes = notesApp.selection();
  notes.forEach((n) => {
    /* 
     * Get plain text and HTML for every selected note
     */
    const text = n.plaintext();
    const html = n.body();

    /*
     * Build folder name for this note 
     */

    const dirName = n.name().replaceAll(/[:/&<>|]/g, "_");
    let targetFolder = `${baseFolder}/${dirName}`;
    if (text || html) {
      /* 
       * Create folder for note data, ignoring already
       * existing folder
       */
      curApp.doShellScript(`mkdir -p "${targetFolder}"`);
      
      /* 
       * Save plain text and HTML using ObjC-Bride 
       */
      if (text) {
        writeUTF8(`${targetFolder}/note.html`, text);
      }
      if (html) {
        writeUTF8(`${targetFolder}/note.html`, html);
      }
    }
  });
})();

function writeUTF8(fileName, text) {
  /* 
   * ObjC-Bridge methods to write text 
   * with UTF8 encoding to file 
   */
  const str = $.NSString.alloc.initWithUTF8String(text);
  str.writeToFileAtomicallyEncodingError(
    fileName, true, $.NSUTF8StringEncoding, null
  );
}

Save attachments of selected notes #

There’s no direct way to export notes from Apple’s program. You can only save notes as PDF, and there’s no provision to save attached files. This makes it difficult to move your documents to another program. The following script saves all attachments for the currently selected notes in sub-folders under the baseFolder, named after the notes.

The saving operation is a bit tricky, since simply using the Notes’s save() method can fail because of missing access rights. In that case, the script inspects the error message and extracts the path to the attachment from it. It then uses the shell’s cp command to copy the attachment file to the new folder.

Note that the shell command can only work if you have granted full disk access to sh in the macOS system preferences’s privacy section. Grant full disk access to sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
(() => {
  const curApp = Application.currentApplication();
  curApp.includeStandardAdditions = true;
  const baseFolder = `${curApp.pathTo("desktop")}/Notes`;

  /*
   * Get currently selected notes
   */
  const notesApp = Application("Notes");
  const notes = notesApp.selection();
  notes.forEach((n) => {
    /*
     * Get attachments for every selected notes
     */
    const attachments = n.attachments();
    if (attachments.length > 0) {
      /*
       * Build the name of the folder to save these attachments * in by taking the note's name and replacing replacing
       * all special characters by underscores.
       */
      const dirName = n.name().replaceAll(/[:/&<>|]/g, "_");
      let targetFolder = `${baseFolder}/${dirName}`;
      /*
       * Create the folder for this note, do nothing
       * if it exists already
       */
      curApp.doShellScript(`mkdir -p "${targetFolder}"`);

      attachments.forEach((a) => {
        /*
         * Get the name of every attachment and try to
         * save it with the Notes method save. This can
         * fail due to issues with the access rights.
         * In that case, us the shell's cp command
         * to copy the data, getting the original file name
         * of the attachment from the error message
         */
        const targetFile = `${targetFolder}/${a.name()}`;
        try {
          notesApp.save(a, { in: targetFile });
        } catch (error) {
          if (/Code=1 "Operation not permitted"/.test(error.message)) {
            const match = error.message.match(
              /NSSourceFilePathErrorKey=(.*),\s/m
            );
            curApp.doShellScript(`cp "${match[1]}" "${targetFile}"`);
          } else {
            curapp.displayAlert(error.message);
          }
        }
      });
    }
  });
})();
About