-
-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathekg-apple-notes.el
More file actions
586 lines (521 loc) · 23.2 KB
/
ekg-apple-notes.el
File metadata and controls
586 lines (521 loc) · 23.2 KB
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
;;; ekg-apple-notes.el --- Bidirectional sync between ekg and Apple Notes -*- lexical-binding: t -*-
;; Copyright (c) 2026 Andrew Hyatt <ahyatt@gmail.com>
;; SPDX-License-Identifier: GPL-3.0-or-later
;;
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 3 of the
;; License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Bidirectional sync between ekg and Apple Notes on macOS. Notes are
;; stored in a configurable Apple Notes folder. Tags are stored as
;; individual "Tag: <tag>" lines. Content is converted between
;; org-mode/markdown and HTML via pandoc.
;;
;; Usage:
;; (require 'ekg-apple-notes)
;; (ekg-apple-notes-sync)
;;; Code:
(require 'ekg)
(require 'triples)
(require 'seq)
(require 'cl-lib)
(defgroup ekg-apple-notes nil
"Customization for ekg's Apple Notes integration."
:group 'ekg)
(defcustom ekg-apple-notes-folder "ekg"
"Name of the Apple Notes folder used for sync."
:type 'string
:group 'ekg-apple-notes)
(defcustom ekg-apple-notes-account nil
"Apple Notes account to use, or nil for the default account."
:type '(choice (const nil) string)
:group 'ekg-apple-notes)
(defcustom ekg-apple-notes-pandoc-executable "pandoc"
"Path to the pandoc executable for content conversion."
:type 'string
:group 'ekg-apple-notes)
(defcustom ekg-apple-notes-export-tags nil
"If non-nil, only export notes that have at least one of these tags.
If nil, all non-hidden notes are exported."
:type '(repeat string)
:group 'ekg-apple-notes)
(defcustom ekg-apple-notes-exclude-tags nil
"Tags whose notes should be excluded from sync.
Notes with any of these tags will not be exported or imported.
If `ekg-agent' is loaded, its tag variables
`ekg-agent-author-tag' and `ekg-agent-self-info-tag' are
automatically excluded as well."
:type '(repeat string)
:group 'ekg-apple-notes)
(defun ekg-apple-notes--exclude-tags ()
"Return the effective list of tags to exclude from sync.
Merges `ekg-apple-notes-exclude-tags' with the `ekg-agent' tag
variables if `ekg-agent' is loaded."
(seq-uniq
(append ekg-apple-notes-exclude-tags
(when (boundp 'ekg-agent-author-tag)
(list (symbol-value 'ekg-agent-author-tag)))
(when (boundp 'ekg-agent-self-info-tag)
(list (symbol-value 'ekg-agent-self-info-tag))))))
;;; ---- Schema ----
(defun ekg-apple-notes-add-schema ()
"Add the Apple Notes schema to the ekg database."
(triples-add-schema ekg-db 'apple-notes
'(last-export :base/unique t :base/type integer)
'(last-import :base/unique t :base/type integer)
'(note-id :base/unique t :base/type string)
'(folder :base/unique t :base/type string)))
(defun ekg-apple-notes-connect ()
"Connect to ekg and ensure the Apple Notes schema is set up."
(ekg-connect)
(ekg-apple-notes-add-schema))
(defun ekg-apple-notes--get-folder ()
"Return the Apple Notes folder name for this database.
Use the value stored in the database if present, otherwise fall
back to `ekg-apple-notes-folder'."
(or (plist-get (triples-get-type ekg-db 'apple-notes 'apple-notes)
:folder)
ekg-apple-notes-folder))
(defun ekg-apple-notes-set-folder (folder)
"Store FOLDER as the Apple Notes folder for this ekg database.
Future syncs will use this folder instead of `ekg-apple-notes-folder'."
(interactive "sName for the ekg folder in Apple Notes for this database: ")
(ekg-apple-notes-connect)
(let ((plist (triples-get-type ekg-db 'apple-notes 'apple-notes)))
(apply #'triples-set-type ekg-db 'apple-notes 'apple-notes
(plist-put plist :folder folder))))
(defun ekg-apple-notes--get-last-export ()
"Return the last export time as an integer epoch, or 0."
(or (plist-get (triples-get-type ekg-db 'apple-notes 'apple-notes)
:last-export)
0))
(defun ekg-apple-notes--set-last-export (time)
"Set the last export time to TIME."
(let ((plist (triples-get-type ekg-db 'apple-notes 'apple-notes)))
(apply #'triples-set-type ekg-db 'apple-notes 'apple-notes
(plist-put plist :last-export (floor (float-time time))))))
(defun ekg-apple-notes--get-last-import ()
"Return the last import time as an integer epoch, or 0."
(or (plist-get (triples-get-type ekg-db 'apple-notes 'apple-notes)
:last-import)
0))
(defun ekg-apple-notes--set-last-import (time)
"Set the last import time to TIME."
(let ((plist (triples-get-type ekg-db 'apple-notes 'apple-notes)))
(apply #'triples-set-type ekg-db 'apple-notes 'apple-notes
(plist-put plist :last-import (floor (float-time time))))))
;;; ---- ID Mapping ----
(defun ekg-apple-notes--set-apple-id (ekg-id apple-id)
"Store the mapping from EKG-ID to APPLE-ID."
(triples-set-type ekg-db ekg-id 'apple-notes
:note-id apple-id))
(defun ekg-apple-notes--get-apple-id (ekg-id)
"Return the Apple Notes ID for EKG-ID, or nil."
(plist-get (triples-get-type ekg-db ekg-id 'apple-notes) :note-id))
(defun ekg-apple-notes--get-ekg-id (apple-id)
"Return the ekg note ID mapped to APPLE-ID, or nil."
(car (triples-subjects-with-predicate-object
ekg-db 'apple-notes/note-id apple-id)))
(defun ekg-apple-notes-reset-mappings ()
"Clear all Apple ID mappings from the database."
(interactive)
(dolist (subj (triples-subjects-of-type ekg-db 'apple-notes))
(triples-remove-type ekg-db subj 'apple-notes)))
;;; ---- AppleScript Layer ----
(defun ekg-apple-notes--run-applescript (script)
"Run SCRIPT via osascript and return the output string.
Signals an error if osascript fails."
(with-temp-buffer
(let ((exit-code (call-process "osascript" nil t nil "-e" script)))
(if (= exit-code 0)
(string-trim (buffer-string))
(error "AppleScript error (exit %d): %s" exit-code (buffer-string))))))
(defun ekg-apple-notes--folder-ref ()
"Return the AppleScript reference to the sync folder."
(let ((folder (ekg-apple-notes--get-folder)))
(if ekg-apple-notes-account
(format "folder %S of account %S"
folder ekg-apple-notes-account)
(format "folder %S" folder))))
(defun ekg-apple-notes--ensure-folder ()
"Create the sync folder in Apple Notes if it doesn't exist."
(let ((folder (ekg-apple-notes--get-folder))
(acct-clause (if ekg-apple-notes-account
(format " of account %S" ekg-apple-notes-account)
"")))
(ekg-apple-notes--run-applescript
(format "tell application \"Notes\"
launch
try
get folder %S%s
on error
make new folder%s with properties {name:%S}
end try
end tell"
folder acct-clause
acct-clause folder))))
(defun ekg-apple-notes--applescript-escape (str)
"Escape STR for safe embedding in an AppleScript string literal.
Only backslashes and double quotes need escaping in AppleScript."
(replace-regexp-in-string
"\"" "\\\\\""
(replace-regexp-in-string "\\\\" "\\\\\\\\" str)))
(defun ekg-apple-notes--with-body-file (body func)
"Write BODY to a temp file, call FUNC with the file path, then clean up.
FUNC receives the temp file path and should return the result.
This avoids AppleScript string escaping issues for large HTML content."
(let ((temp-file (make-temp-file "ekg-apple-notes-" nil ".html")))
(unwind-protect
(progn
(with-temp-file temp-file
(insert body))
(funcall func temp-file))
(when (file-exists-p temp-file)
(delete-file temp-file)))))
(defun ekg-apple-notes--create-note (title body)
"Create a note with TITLE and BODY in the sync folder.
Returns the Apple Notes ID of the new note."
(ekg-apple-notes--with-body-file body
(lambda (body-file)
(ekg-apple-notes--run-applescript
(format "tell application \"Notes\"
launch
set bodyText to read POSIX file %S as «class utf8»
set theNote to make new note at %s with properties {name:\"%s\", body:bodyText}
return id of theNote
end tell"
body-file
(ekg-apple-notes--folder-ref)
(ekg-apple-notes--applescript-escape title))))))
(defun ekg-apple-notes--update-note (apple-id body)
"Update the note with APPLE-ID to have the given BODY."
(ekg-apple-notes--with-body-file body
(lambda (body-file)
(ekg-apple-notes--run-applescript
(format "tell application \"Notes\"
launch
set bodyText to read POSIX file %S as «class utf8»
set body of note id %S to bodyText
end tell"
body-file apple-id)))))
(defun ekg-apple-notes--delete-note (apple-id)
"Delete the note with APPLE-ID. Move it to Recently Deleted."
(condition-case nil
(ekg-apple-notes--run-applescript
(format "tell application \"Notes\"
launch
delete note id %S
end tell"
apple-id))
(error nil)))
(cl-defstruct ekg-apple-notes--note
"Representation of an Apple Notes note for sync."
id name body modification-date)
(defun ekg-apple-notes--list-notes ()
"Return a list of `ekg-apple-notes--note' for all notes in the sync folder."
(let* ((raw (condition-case nil
(ekg-apple-notes--run-applescript
(format "tell application \"Notes\"
launch
set noteData to {}
repeat with n in notes of %s
set noteId to id of n
set noteName to name of n
set noteBody to body of n
set noteMod to modification date of n as «class isot» as string
set end of noteData to noteId & \"|||\" & noteName & \"|||\" & noteMod & \"|||\" & noteBody
end repeat
set AppleScript's text item delimiters to \"###NOTE###\"
return noteData as string
end tell"
(ekg-apple-notes--folder-ref)))
(error "")))
(entries (if (string-empty-p raw) nil
(split-string raw "###NOTE###"))))
(delq nil
(mapcar
(lambda (entry)
(when (string-match "\\`\\(x-coredata://[^|]+\\)|||\\([^|]*\\)|||\\([^|]+\\)|||\\(\\(?:.\\|\n\\)*\\)\\'" entry)
(make-ekg-apple-notes--note
:id (match-string 1 entry)
:name (match-string 2 entry)
:modification-date (match-string 3 entry)
:body (match-string 4 entry))))
entries))))
;;; ---- Tag Conversion ----
(defun ekg-apple-notes--tags-to-metadata (tags)
"Convert a list of ekg TAGS to metadata lines for the note body.
Hidden tags are excluded. Each tag gets its own \"Tag: <tag>\" line."
(let ((visible-tags (seq-difference tags ekg-hidden-tags)))
(when visible-tags
(mapconcat (lambda (tag) (concat "Tag: " tag))
visible-tags "\n"))))
(defun ekg-apple-notes--parse-tags-from-body (body)
"Parse \"Tag: <tag>\" lines from BODY.
Returns a list of tag strings. Each tag occupies its own line in
the format \"Tag: <tag>\", possibly wrapped in HTML div elements."
(let ((tags nil))
(with-temp-buffer
(insert body)
(goto-char (point-min))
(while (re-search-forward
"\\(?:<div>\\)?Tag: \\([^<\n]+?\\)\\(?:</div>\\|$\\)" nil t)
(push (string-trim (match-string 1)) tags)))
(nreverse tags)))
(defun ekg-apple-notes--remove-tags-line (text)
"Remove \"Tag: <tag>\" lines from TEXT."
(replace-regexp-in-string "\\(?:\n\\)?Tag: [^\n]+" "" text))
(defun ekg-apple-notes--parse-resource-from-body (body)
"Parse a \"Resource: <url>\" line from BODY.
Returns the resource string, or nil if not found."
(when (string-match
"\\(?:<div>\\)?Resource: \\([^<\n]+?\\)\\(?:</div>\\|$\\)" body)
(string-trim (match-string 1 body))))
(defun ekg-apple-notes--remove-resource-html (html)
"Remove the \"Resource:\" div and its trailing spacer from HTML."
(let ((html (replace-regexp-in-string
"<div>Resource: [^<]*</div>\n?\\(?:<div><br></div>\n?\\)?"
"" html)))
html))
(defun ekg-apple-notes--remove-tags-html (html)
"Remove \"Tag: <tag>\" divs and their preceding spacer from HTML.
This should be called before pandoc conversion to avoid artifacts."
(let ((html (replace-regexp-in-string
"<div><br></div>\n?\\(?:<div>Tag: [^<]*</div>\n?\\)+"
"" html)))
;; Handle case where there's no spacer div.
(replace-regexp-in-string
"<div>Tag: [^<]*</div>\n?" "" html)))
;;; ---- Content Conversion ----
(defun ekg-apple-notes--pandoc (input from to)
"Convert INPUT string from format FROM to format TO using pandoc."
(unless (executable-find ekg-apple-notes-pandoc-executable)
(error "Pandoc not found; install it or set `ekg-apple-notes-pandoc-executable'"))
(with-temp-buffer
(let ((exit-code
(call-process-region input nil
ekg-apple-notes-pandoc-executable
nil t nil
"-f" from "-t" to
"--wrap=none")))
(if (= exit-code 0)
(string-trim (buffer-string))
(error "Pandoc conversion failed (%s→%s, exit %d): %s"
from to exit-code (buffer-string))))))
(defun ekg-apple-notes--html-delink (html)
"Replace <a href=URL>text</a> in HTML with [text](URL).
Apple Notes strips link hrefs, so we inline them before sending."
(replace-regexp-in-string
"<a[^>]*href=\"\\([^\"]*\\)\"[^>]*>\\([^<]*\\)</a>"
"[\\2](\\1)"
html))
(defun ekg-apple-notes--note-title (note)
"Return a title for the ekg NOTE.
Uses the :titled/title property if available, otherwise a
truncated version of the text."
(or (let ((title (plist-get (ekg-note-properties note) :titled/title)))
(if (listp title) (car title) title))
(truncate-string-to-width
(replace-regexp-in-string "\n" " " (or (ekg-note-text note) ""))
80 nil nil "…")))
(defun ekg-apple-notes--to-html (note)
"Convert an ekg NOTE's text to HTML for Apple Notes.
Handles `org-mode' and markdown via pandoc, with links inlined as
markdown format [text](url)."
(let* ((text (or (ekg-note-text note) ""))
(mode (ekg-note-mode note))
(from (pcase mode
('org-mode "org")
('markdown-mode "markdown")
(_ "markdown")))
(html (ekg-apple-notes--pandoc text from "html"))
(html (ekg-apple-notes--html-delink html))
(tags-meta (ekg-apple-notes--tags-to-metadata (ekg-note-tags note))))
(concat (when (ekg-should-show-id-p (ekg-note-id note))
(format "<div>Resource: %s</div>\n<div><br></div>\n"
(ekg-note-id note)))
html
(when tags-meta
(concat "\n<div><br></div>\n"
(mapconcat (lambda (line) (concat "<div>" line "</div>"))
(split-string tags-meta "\n")
"\n"))))))
(defun ekg-apple-notes--relink-org (text)
"Convert [text](url) patterns in TEXT to `org-mode' links [[url][text]].
Does not handle nested brackets or parentheses in link text or URLs."
(replace-regexp-in-string
"\\[\\([^]]+\\)\\](\\([^)]+\\))"
"[[\\2][\\1]]"
text))
(defun ekg-apple-notes--normalize-divs (html)
"Convert <div> wrappers to <p> in HTML.
Apple Notes wraps every line in <div> tags, which pandoc passes
through as raw HTML when converting to markdown. Converting to
<p> gives pandoc proper paragraph structure to work with.
Also strips trailing <br> inside divs, which Apple Notes adds
when users edit notes and which pandoc would convert to
backslash line breaks."
(let ((html (replace-regexp-in-string "<div><br></div>" "" html)))
(setq html (replace-regexp-in-string "<br ?/?>\\(</div>\\)" "\\1" html))
(setq html (replace-regexp-in-string "<div>" "<p>" html))
(replace-regexp-in-string "</div>" "</p>" html)))
(defun ekg-apple-notes--from-html (body mode)
"Convert Apple Notes BODY (HTML) to text in MODE.
MODE should be the symbol `org-mode' or `markdown-mode'."
(let* ((body (ekg-apple-notes--remove-resource-html body))
(body (ekg-apple-notes--remove-tags-html body))
(body (ekg-apple-notes--normalize-divs body))
(to (pcase mode
('org-mode "org")
('markdown-mode "markdown")
(_ "markdown")))
(text (ekg-apple-notes--pandoc body "html" to)))
(when (eq mode 'org-mode)
(setq text (ekg-apple-notes--relink-org text)))
(string-trim text)))
;;; ---- Export (ekg → Apple Notes) ----
(defun ekg-apple-notes--notes-to-export (since)
"Return ekg notes modified since SINCE (integer epoch).
Respects `ekg-apple-notes-export-tags' if set."
(let* ((pred (if (= 0 since) :time-tracked/creation-time :time-tracked/modified-time))
(rows (triples-db-select-pred-op ekg-db pred '>= since))
(ids (seq-uniq (mapcar #'car rows)))
(notes (delq nil (mapcar #'ekg-get-note-with-id ids))))
(setq notes (seq-filter #'ekg-note-active-p notes))
(let ((excluded (ekg-apple-notes--exclude-tags)))
(setq notes (seq-filter
(lambda (note)
(not (seq-intersection (ekg-note-tags note) excluded)))
notes)))
(when ekg-apple-notes-export-tags
(setq notes (seq-filter
(lambda (note)
(seq-intersection (ekg-note-tags note)
ekg-apple-notes-export-tags))
notes)))
notes))
(defun ekg-apple-notes--export-note (note)
"Export a single ekg NOTE to Apple Notes.
Creates or updates the corresponding Apple Note."
(let* ((title (ekg-apple-notes--note-title note))
(body (ekg-apple-notes--to-html note))
(apple-id (ekg-apple-notes--get-apple-id (ekg-note-id note))))
(if apple-id
;; Update existing note.
(progn
(ekg-apple-notes--update-note apple-id body)
(message "ekg-apple-notes: updated note %s" title))
;; Create new note.
(setq apple-id (ekg-apple-notes--create-note title body))
(ekg-apple-notes--set-apple-id (ekg-note-id note) apple-id)
(message "ekg-apple-notes: created note %s" title))))
(defun ekg-apple-notes-export (&optional force)
"Export modified ekg notes to Apple Notes.
With FORCE (prefix arg), re-export all notes regardless of
modification time."
(interactive "P")
(ekg-apple-notes-connect)
(ekg-apple-notes--ensure-folder)
(let* ((last-export (if force 0 (ekg-apple-notes--get-last-export)))
(notes (ekg-apple-notes--notes-to-export last-export))
(count 0))
(message "ekg-apple-notes: exporting %d notes modified since %s"
(length notes)
(if (= 0 last-export) "the beginning"
(format-time-string "%F %X" last-export)))
(dolist (note notes)
(condition-case err
(progn
(ekg-apple-notes--export-note note)
(cl-incf count))
(error (message "ekg-apple-notes: failed to export note %S: %s"
(ekg-note-id note) (error-message-string err)))))
(message "ekg-apple-notes: exported %d notes" count)
(ekg-apple-notes--set-last-export (current-time))))
;;; ---- Import (Apple Notes → ekg) ----
(defun ekg-apple-notes--parse-iso-time (iso-str)
"Parse ISO-STR (e.g. 2026-02-24T14:30:00) to an integer epoch."
(floor (float-time (encode-time (iso8601-parse iso-str)))))
(defun ekg-apple-notes--import-note (apple-note)
"Import a single APPLE-NOTE into ekg.
APPLE-NOTE is an `ekg-apple-notes--note' struct.
Returns non-nil if a note was created or updated."
(let* ((apple-id (ekg-apple-notes--note-id apple-note))
(body (ekg-apple-notes--note-body apple-note))
(ekg-id (ekg-apple-notes--get-ekg-id apple-id))
(resource (ekg-apple-notes--parse-resource-from-body body))
(tags (ekg-apple-notes--parse-tags-from-body body))
(mode ekg-capture-default-mode)
(text (ekg-apple-notes--from-html body mode)))
(if ekg-id
;; Existing note — only import if modified after our last export.
(let ((mod-time (ekg-apple-notes--parse-iso-time
(ekg-apple-notes--note-modification-date apple-note)))
(last-export (ekg-apple-notes--get-last-export)))
(if (<= mod-time last-export)
nil ;; Modified by our export, not externally.
(let ((note (ekg-get-note-with-id ekg-id)))
(when note
(setf (ekg-note-text note) text)
(when tags
(setf (ekg-note-tags note) tags))
(ekg-save-note note)
(message "ekg-apple-notes: updated ekg note from Apple Notes %s"
apple-id)
t))))
;; New note from Apple Notes — skip if tags match exclusions.
(unless (seq-intersection (or tags '("imported"))
(ekg-apple-notes--exclude-tags))
(let ((note (ekg-note-create
:text text
:mode mode
:tags (or tags '("imported"))
:id resource)))
(ekg-save-note note)
(ekg-apple-notes--set-apple-id (ekg-note-id note) apple-id)
(message "ekg-apple-notes: imported new note from Apple Notes %s"
apple-id)
t)))))
(defun ekg-apple-notes-import ()
"Import new and modified notes from Apple Notes into ekg."
(interactive)
(ekg-apple-notes-connect)
(let* ((last-import (ekg-apple-notes--get-last-import))
(start-time (current-time))
(apple-notes (ekg-apple-notes--list-notes))
(count 0))
(message "ekg-apple-notes: checking %d notes in Apple Notes folder"
(length apple-notes))
(dolist (an apple-notes)
(let ((mod-time (ekg-apple-notes--parse-iso-time
(ekg-apple-notes--note-modification-date an))))
(when (>= mod-time last-import)
(condition-case err
(when (ekg-apple-notes--import-note an)
(cl-incf count))
(error (message "ekg-apple-notes: failed to import note %s: %s"
(ekg-apple-notes--note-id an)
(error-message-string err)))))))
(message "ekg-apple-notes: imported %d notes" count)
(ekg-apple-notes--set-last-import start-time)))
;;; ---- Sync ----
;;;###autoload
(defun ekg-apple-notes-sync (&optional force)
"Bidirectional sync between ekg and Apple Notes.
Imports from Apple Notes first, then exports to Apple Notes.
With FORCE (prefix arg), re-export all notes."
(interactive "P")
(ekg-apple-notes-connect)
(ekg-apple-notes-import)
(ekg-apple-notes-export force))
(provide 'ekg-apple-notes)
;;; ekg-apple-notes.el ends here