Protect the bidi iterator against zero bidi properties.
[bpt/emacs.git] / lisp / org / org-mobile.el
CommitLineData
8d642074 1;;; org-mobile.el --- Code for asymmetric sync with a mobile device
3ab2c837 2;; Copyright (C) 2009, 2010 Free Software Foundation, Inc.
8d642074
CD
3;;
4;; Author: Carsten Dominik <carsten at orgmode dot org>
5;; Keywords: outlines, hypermedia, calendar, wp
6;; Homepage: http://orgmode.org
3ab2c837 7;; Version: 7.7
8d642074
CD
8;;
9;; This file is part of GNU Emacs.
10;;
11;; GNU Emacs is free software: you can redistribute it and/or modify
12;; it under the terms of the GNU General Public License as published by
13;; the Free Software Foundation, either version 3 of the License, or
14;; (at your option) any later version.
15;;
16;; GNU Emacs is distributed in the hope that it will be useful,
17;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19;; GNU General Public License for more details.
20;;
21;; You should have received a copy of the GNU General Public License
22;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
23;;
24;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
25;;
26;;; Commentary:
27;;
28;; This file contains the code to interact with Richard Moreland's iPhone
afe98dfa
CD
29;; application MobileOrg, as well as with the Android version by Matthew Jones.
30;; This code is documented in Appendix B of the Org-mode manual. The code is
31;; not specific for the iPhone and Android - any external
32;; viewer/flagging/editing application that uses the same conventions could
33;; be used.
8d642074
CD
34
35(require 'org)
36(require 'org-agenda)
86fbb8ca
CD
37;;; Code:
38
8bfe682a 39(eval-when-compile (require 'cl))
8d642074 40
3ab2c837
BG
41(declare-function org-pop-to-buffer-same-window
42 "org-compat" (&optional buffer-or-name norecord label))
43
8d642074 44(defgroup org-mobile nil
8bfe682a 45 "Options concerning support for a viewer/editor on a mobile device."
8d642074
CD
46 :tag "Org Mobile"
47 :group 'org)
48
49(defcustom org-mobile-files '(org-agenda-files)
50 "Files to be staged for MobileOrg.
8bfe682a 51This is basically a list of files and directories. Files will be staged
8d642074
CD
52directly. Directories will be search for files with the extension `.org'.
53In addition to this, the list may also contain the following symbols:
54
55org-agenda-files
ed21c5c8 56 This means include the complete, unrestricted list of files given in
8d642074
CD
57 the variable `org-agenda-files'.
58org-agenda-text-search-extra-files
59 Include the files given in the variable
60 `org-agenda-text-search-extra-files'"
61 :group 'org-mobile
62 :type '(list :greedy t
63 (option (const :tag "org-agenda-files" org-agenda-files))
64 (option (const :tag "org-agenda-text-search-extra-files"
65 org-agenda-text-search-extra-files))
66 (repeat :inline t :tag "Additional files"
67 (file))))
68
3ab2c837
BG
69(defcustom org-mobile-files-exclude-regexp ""
70 "A regexp to exclude files from `org-mobile-files'."
71 :group 'org-mobile
72 :type 'regexp)
73
8d642074
CD
74(defcustom org-mobile-directory ""
75 "The WebDAV directory where the interaction with the mobile takes place."
76 :group 'org-mobile
77 :type 'directory)
78
ed21c5c8 79(defcustom org-mobile-use-encryption nil
86fbb8ca 80 "Non-nil means keep only encrypted files on the WebDAV server.
ed21c5c8
CD
81Encryption uses AES-256, with a password given in
82`org-mobile-encryption-password'.
83When nil, plain files are kept on the server.
84Turning on encryption requires to set the same password in the MobileOrg
86fbb8ca
CD
85application. Before turning this on, check of MobileOrg does already
86support it - at the time of this writing it did not yet."
ed21c5c8
CD
87 :group 'org-mobile
88 :type 'boolean)
89
90(defcustom org-mobile-encryption-tempfile "~/orgtmpcrypt"
91 "File that is being used as a temporary file for encryption.
86fbb8ca 92This must be local file on your local machine (not on the WebDAV server).
ed21c5c8
CD
93You might want to put this file into a directory where only you have access."
94 :group 'org-mobile
95 :type 'directory)
96
97(defcustom org-mobile-encryption-password ""
98 "Password for encrypting files uploaded to the server.
99This is a single password which is used for AES-256 encryption. The same
100password must also be set in the MobileOrg application. All Org files,
101including mobileorg.org will be encrypted using this password.
afe98dfa
CD
102
103SECURITY CONSIDERATIONS:
104
86fbb8ca 105Note that, when Org runs the encryption commands, the password could
afe98dfa
CD
106be visible briefly on your system with the `ps' command. So this method is
107only intended to keep the files secure on the server, not on your own machine.
108
109Also, if you set this variable in an init file (.emacs or .emacs.d/init.el
110or custom.el...) and if that file is stored in a way so that other can read
111it, this also limits the security of this approach. You can also leave
112this variable empty - Org will then ask for the password once per Emacs
113session."
ed21c5c8
CD
114 :group 'org-mobile
115 :type '(string :tag "Password"))
116
afe98dfa
CD
117(defvar org-mobile-encryption-password-session nil)
118
119(defun org-mobile-encryption-password ()
120 (or (org-string-nw-p org-mobile-encryption-password)
121 (org-string-nw-p org-mobile-encryption-password-session)
122 (setq org-mobile-encryption-password-session
123 (read-passwd "Password for MobileOrg: " t))))
124
8d642074
CD
125(defcustom org-mobile-inbox-for-pull "~/org/from-mobile.org"
126 "The file where captured notes and flags will be appended to.
127During the execution of `org-mobile-pull', the file
128`org-mobile-capture-file' will be emptied it's contents have
8bfe682a
CD
129been appended to the file given here. This file should be in
130`org-directory', and not in the staging area or on the web server."
8d642074
CD
131 :group 'org-mobile
132 :type 'file)
133
134(defconst org-mobile-capture-file "mobileorg.org"
135 "The capture file where the mobile stores captured notes and flags.
136This should not be changed, because MobileOrg assumes this name.")
137
138(defcustom org-mobile-index-file "index.org"
3ab2c837 139 "The index file with links to all Org files that should be loaded by MobileOrg.
8d642074
CD
140Relative to `org-mobile-directory'. The Address field in the MobileOrg setup
141should point to this file."
142 :group 'org-mobile
143 :type 'file)
144
ed21c5c8
CD
145(defcustom org-mobile-agendas 'all
146 "The agendas that should be pushed to MobileOrg.
147Allowed values:
148
149default the weekly agenda and the global TODO list
150custom all custom agendas defined by the user
151all the custom agendas and the default ones
152list a list of selection key(s) as string."
153 :group 'org-mobile
154 :type '(choice
155 (const :tag "Default Agendas" default)
156 (const :tag "Custom Agendas" custom)
157 (const :tag "Default and Custom Agendas" all)
158 (repeat :tag "Selected"
159 (string :tag "Selection Keys"))))
160
8d642074 161(defcustom org-mobile-force-id-on-agenda-items t
afe98dfa 162 "Non-nil means make all agenda items carry an ID."
8d642074
CD
163 :group 'org-mobile
164 :type 'boolean)
165
8bfe682a 166(defcustom org-mobile-force-mobile-change nil
ed21c5c8 167 "Non-nil means force the change made on the mobile device.
8bfe682a
CD
168So even if there have been changes to the computer version of the entry,
169force the new value set on the mobile.
170When nil, mark the entry from the mobile with an error message.
171Instead of nil or t, this variable can also be a list of symbols, indicating
172the editing types for which the mobile version should always dominate."
173 :group 'org-mobile
174 :type '(choice
175 (const :tag "Always" t)
176 (const :tag "Never" nil)
177 (set :greedy t :tag "Specify"
178 (const todo)
179 (const tags)
180 (const priority)
181 (const heading)
182 (const body))))
183
8d642074 184(defcustom org-mobile-action-alist
8bfe682a 185 '(("edit" . (org-mobile-edit data old new)))
8d642074
CD
186 "Alist with flags and actions for mobile sync.
187When flagging an entry, MobileOrg will create entries that look like
188
189 * F(action:data) [[id:entry-id][entry title]]
190
191This alist defines that the ACTION in the parentheses of F() should mean,
192i.e. what action should be taken. The :data part in the parenthesis is
193optional. If present, the string after the colon will be passed to the
194action form as the `data' variable.
195The car of each elements of the alist is an actions string. The cdr is
196an Emacs Lisp form that will be evaluated with the cursor on the headline
8bfe682a
CD
197of that entry.
198
199For now, it is not recommended to change this variable."
8d642074
CD
200 :group 'org-mobile
201 :type '(repeat
202 (cons (string :tag "Action flag")
203 (sexp :tag "Action form"))))
204
8bfe682a
CD
205(defcustom org-mobile-checksum-binary (or (executable-find "shasum")
206 (executable-find "sha1sum")
207 (executable-find "md5sum")
208 (executable-find "md5"))
209 "Executable used for computing checksums of agenda files."
210 :group 'org-mobile
211 :type 'string)
212
8d642074
CD
213(defvar org-mobile-pre-push-hook nil
214 "Hook run before running `org-mobile-push'.
215This could be used to clean up `org-mobile-directory', for example to
216remove files that used to be included in the agenda but no longer are.
217The presence of such files would not really be a problem, but after time
218they may accumulate.")
219
220(defvar org-mobile-post-push-hook nil
221 "Hook run after running `org-mobile-push'.
222If Emacs does not have direct write access to the WebDAV directory used
223by the mobile device, this hook should be used to copy all files from the
224local staging directory `org-mobile-directory' to the WebDAV directory,
225for example using `rsync' or `scp'.")
226
227(defvar org-mobile-pre-pull-hook nil
228 "Hook run before executing `org-mobile-pull'.
229If Emacs does not have direct write access to the WebDAV directory used
230by the mobile device, this hook should be used to copy the capture file
231`mobileorg.org' from the WebDAV location to the local staging
232directory `org-mobile-directory'.")
233
234(defvar org-mobile-post-pull-hook nil
235 "Hook run after running `org-mobile-pull'.
236If Emacs does not have direct write access to the WebDAV directory used
237by the mobile device, this hook should be used to copy the emptied
238capture file `mobileorg.org' back to the WebDAV directory, for example
239using `rsync' or `scp'.")
240
241(defvar org-mobile-last-flagged-files nil
8bfe682a 242 "List of files containing entries flagged in the latest pull.")
8d642074
CD
243
244(defvar org-mobile-files-alist nil)
245(defvar org-mobile-checksum-files nil)
246
247(defun org-mobile-prepare-file-lists ()
248 (setq org-mobile-files-alist (org-mobile-files-alist))
8bfe682a 249 (setq org-mobile-checksum-files nil))
8d642074
CD
250
251(defun org-mobile-files-alist ()
3ab2c837
BG
252 "Expand the list in `org-mobile-files' to a list of existing files.
253Also exclude files matching `org-mobile-files-exclude-regexp'."
8bfe682a
CD
254 (let* ((include-archives
255 (and (member 'org-agenda-text-search-extra-files org-mobile-files)
256 (member 'agenda-archives org-agenda-text-search-extra-files)
257 t))
258 (files
259 (apply 'append
260 (mapcar
261 (lambda (f)
262 (cond
263 ((eq f 'org-agenda-files)
264 (org-agenda-files t include-archives))
265 ((eq f 'org-agenda-text-search-extra-files)
266 (delq 'agenda-archives
267 (copy-sequence
268 org-agenda-text-search-extra-files)))
269 ((and (stringp f) (file-directory-p f))
270 (directory-files f 'full "\\.org\\'"))
271 ((and (stringp f) (file-exists-p f))
272 (list f))
273 (t nil)))
274 org-mobile-files)))
3ab2c837
BG
275 (files (delete
276 nil
277 (mapcar (lambda (f)
278 (unless (and (not (string= org-mobile-files-exclude-regexp ""))
279 (string-match org-mobile-files-exclude-regexp f))
280 (identity f)))
281 files)))
8d642074
CD
282 (orgdir-uname (file-name-as-directory (file-truename org-directory)))
283 (orgdir-re (concat "\\`" (regexp-quote orgdir-uname)))
284 uname seen rtn file link-name)
285 ;; Make the files unique, and determine the name under which they will
286 ;; be listed.
287 (while (setq file (pop files))
8bfe682a
CD
288 (if (not (file-name-absolute-p file))
289 (setq file (expand-file-name file org-directory)))
8d642074
CD
290 (setq uname (file-truename file))
291 (unless (member uname seen)
292 (push uname seen)
293 (if (string-match orgdir-re uname)
294 (setq link-name (substring uname (match-end 0)))
295 (setq link-name (file-name-nondirectory uname)))
296 (push (cons file link-name) rtn)))
297 (nreverse rtn)))
298
299;;;###autoload
300(defun org-mobile-push ()
301 "Push the current state of Org affairs to the WebDAV directory.
302This will create the index file, copy all agenda files there, and also
303create all custom agenda views, for upload to the mobile phone."
304 (interactive)
8bfe682a
CD
305 (let ((a-buffer (get-buffer org-agenda-buffer-name)))
306 (let ((org-agenda-buffer-name "*SUMO*")
307 (org-agenda-filter org-agenda-filter)
308 (org-agenda-redo-command org-agenda-redo-command))
309 (save-excursion
310 (save-window-excursion
3ab2c837 311 (run-hooks 'org-mobile-pre-push-hook)
8bfe682a
CD
312 (org-mobile-check-setup)
313 (org-mobile-prepare-file-lists)
8bfe682a
CD
314 (message "Creating agendas...")
315 (let ((inhibit-redisplay t)) (org-mobile-create-sumo-agenda))
316 (message "Creating agendas...done")
317 (org-save-all-org-buffers) ; to save any IDs created by this process
318 (message "Copying files...")
319 (org-mobile-copy-agenda-files)
320 (message "Writing index file...")
321 (org-mobile-create-index-file)
322 (message "Writing checksums...")
323 (org-mobile-write-checksums)
324 (run-hooks 'org-mobile-post-push-hook))))
325 (redraw-display)
326 (when (and a-buffer (buffer-live-p a-buffer))
327 (if (not (get-buffer-window a-buffer))
328 (kill-buffer a-buffer)
329 (let ((cw (selected-window)))
330 (select-window (get-buffer-window a-buffer))
8bfe682a
CD
331 (org-agenda-redo)
332 (select-window cw)))))
8d642074 333 (message "Files for mobile viewer staged"))
ed21c5c8 334
8bfe682a
CD
335(defvar org-mobile-before-process-capture-hook nil
336 "Hook that is run after content was moved to `org-mobile-inbox-for-pull'.
ed21c5c8
CD
337The inbox file is visited by the current buffer, and the buffer is
338narrowed to the newly captured data.")
8d642074
CD
339
340;;;###autoload
341(defun org-mobile-pull ()
342 "Pull the contents of `org-mobile-capture-file' and integrate them.
343Apply all flagged actions, flag entries to be flagged and then call an
344agenda view showing the flagged items."
345 (interactive)
346 (org-mobile-check-setup)
347 (run-hooks 'org-mobile-pre-pull-hook)
348 (let ((insertion-marker (org-mobile-move-capture)))
349 (if (not (markerp insertion-marker))
350 (message "No new items")
351 (org-with-point-at insertion-marker
8bfe682a
CD
352 (save-restriction
353 (narrow-to-region (point) (point-max))
354 (run-hooks 'org-mobile-before-process-capture-hook)))
355 (org-with-point-at insertion-marker
356 (org-mobile-apply (point) (point-max)))
8d642074
CD
357 (move-marker insertion-marker nil)
358 (run-hooks 'org-mobile-post-pull-hook)
359 (when org-mobile-last-flagged-files
360 ;; Make an agenda view of flagged entries, but only in the files
361 ;; where stuff has been added.
362 (put 'org-agenda-files 'org-restrict org-mobile-last-flagged-files)
8bfe682a 363 (let ((org-agenda-keep-restricted-file-list t))
8d642074
CD
364 (org-agenda nil "?"))))))
365
366(defun org-mobile-check-setup ()
367 "Check if org-mobile-directory has been set up."
afe98dfa 368 (org-mobile-cleanup-encryption-tempfile)
8bfe682a
CD
369 (unless (and org-directory
370 (stringp org-directory)
371 (string-match "\\S-" org-directory)
372 (file-exists-p org-directory)
373 (file-directory-p org-directory))
374 (error
375 "Please set `org-directory' to the directory where your org files live"))
376 (unless (and org-mobile-directory
377 (stringp org-mobile-directory)
378 (string-match "\\S-" org-mobile-directory)
379 (file-exists-p org-mobile-directory)
380 (file-directory-p org-mobile-directory))
8d642074
CD
381 (error
382 "Variable `org-mobile-directory' must point to an existing directory"))
8bfe682a
CD
383 (unless (and org-mobile-inbox-for-pull
384 (stringp org-mobile-inbox-for-pull)
385 (string-match "\\S-" org-mobile-inbox-for-pull)
386 (file-exists-p
387 (file-name-directory org-mobile-inbox-for-pull)))
8d642074 388 (error
ed21c5c8 389 "Variable `org-mobile-inbox-for-pull' must point to a file in an existing directory"))
86fbb8ca
CD
390 (unless (and org-mobile-checksum-binary
391 (string-match "\\S-" org-mobile-checksum-binary))
392 (error "No executable found to compute checksums"))
ed21c5c8 393 (when org-mobile-use-encryption
afe98dfa 394 (unless (string-match "\\S-" (org-mobile-encryption-password))
ed21c5c8
CD
395 (error
396 "To use encryption, you must set `org-mobile-encryption-password'"))
397 (unless (file-writable-p org-mobile-encryption-tempfile)
86fbb8ca 398 (error "Cannot write to encryption tempfile %s"
ed21c5c8
CD
399 org-mobile-encryption-tempfile))
400 (unless (executable-find "openssl")
86fbb8ca 401 (error "openssl is needed to encrypt files"))))
8d642074
CD
402
403(defun org-mobile-create-index-file ()
404 "Write the index file in the WebDAV directory."
8bfe682a
CD
405 (let ((files-alist (sort (copy-sequence org-mobile-files-alist)
406 (lambda (a b) (string< (cdr a) (cdr b)))))
407 (def-todo (default-value 'org-todo-keywords))
408 (def-tags (default-value 'org-tag-alist))
afe98dfa
CD
409 (target-file (expand-file-name org-mobile-index-file
410 org-mobile-directory))
8bfe682a 411 file link-name todo-kwds done-kwds tags drawers entry kwds dwds twds)
ed21c5c8 412
8d642074
CD
413 (org-prepare-agenda-buffers (mapcar 'car files-alist))
414 (setq done-kwds (org-uniquify org-done-keywords-for-agenda))
415 (setq todo-kwds (org-delete-all
416 done-kwds
417 (org-uniquify org-todo-keywords-for-agenda)))
418 (setq drawers (org-uniquify org-drawers-for-agenda))
419 (setq tags (org-uniquify
420 (delq nil
421 (mapcar
422 (lambda (e)
423 (cond ((stringp e) e)
424 ((listp e)
425 (if (stringp (car e)) (car e) nil))
426 (t nil)))
427 org-tag-alist-for-agenda))))
428 (with-temp-file
afe98dfa
CD
429 (if org-mobile-use-encryption
430 org-mobile-encryption-tempfile
431 target-file)
8bfe682a
CD
432 (while (setq entry (pop def-todo))
433 (insert "#+READONLY\n")
434 (setq kwds (mapcar (lambda (x) (if (string-match "(" x)
435 (substring x 0 (match-beginning 0))
436 x))
437 (cdr entry)))
438 (insert "#+TODO: " (mapconcat 'identity kwds " ") "\n")
439 (setq dwds (member "|" kwds)
440 twds (org-delete-all dwds kwds)
441 todo-kwds (org-delete-all twds todo-kwds)
442 done-kwds (org-delete-all dwds done-kwds)))
443 (when (or todo-kwds done-kwds)
444 (insert "#+TODO: " (mapconcat 'identity todo-kwds " ") " | "
445 (mapconcat 'identity done-kwds " ") "\n"))
446 (setq def-tags (mapcar
447 (lambda (x)
448 (cond ((null x) nil)
449 ((stringp x) x)
450 ((eq (car x) :startgroup) "{")
451 ((eq (car x) :endgroup) "}")
452 ((eq (car x) :newline) nil)
453 ((listp x) (car x))
454 (t nil)))
455 def-tags))
456 (setq def-tags (delq nil def-tags))
457 (setq tags (org-delete-all def-tags tags))
458 (setq tags (sort tags (lambda (a b) (string< (downcase a) (downcase b)))))
459 (setq tags (append def-tags tags nil))
460 (insert "#+TAGS: " (mapconcat 'identity tags " ") "\n")
461 (insert "#+DRAWERS: " (mapconcat 'identity drawers " ") "\n")
462 (insert "#+ALLPRIORITIES: A B C" "\n")
463 (when (file-exists-p (expand-file-name
464 org-mobile-directory "agendas.org"))
465 (insert "* [[file:agendas.org][Agenda Views]]\n"))
8d642074
CD
466 (while (setq entry (pop files-alist))
467 (setq file (car entry)
468 link-name (cdr entry))
469 (insert (format "* [[file:%s][%s]]\n"
470 link-name link-name)))
8bfe682a 471 (push (cons org-mobile-index-file (md5 (buffer-string)))
afe98dfa
CD
472 org-mobile-checksum-files))
473 (when org-mobile-use-encryption
474 (org-mobile-encrypt-and-move org-mobile-encryption-tempfile
475 target-file)
476 (org-mobile-cleanup-encryption-tempfile))))
8d642074
CD
477
478(defun org-mobile-copy-agenda-files ()
479 "Copy all agenda files to the stage or WebDAV directory."
480 (let ((files-alist org-mobile-files-alist)
8bfe682a 481 file buf entry link-name target-path target-dir check)
8d642074
CD
482 (while (setq entry (pop files-alist))
483 (setq file (car entry) link-name (cdr entry))
484 (when (file-exists-p file)
485 (setq target-path (expand-file-name link-name org-mobile-directory)
486 target-dir (file-name-directory target-path))
487 (unless (file-directory-p target-dir)
8bfe682a 488 (make-directory target-dir 'parents))
ed21c5c8
CD
489 (if org-mobile-use-encryption
490 (org-mobile-encrypt-and-move file target-path)
491 (copy-file file target-path 'ok-if-exists))
8bfe682a
CD
492 (setq check (shell-command-to-string
493 (concat org-mobile-checksum-binary " "
494 (shell-quote-argument (expand-file-name file)))))
495 (when (string-match "[a-fA-F0-9]\\{30,40\\}" check)
496 (push (cons link-name (match-string 0 check))
497 org-mobile-checksum-files))))
afe98dfa 498
8d642074
CD
499 (setq file (expand-file-name org-mobile-capture-file
500 org-mobile-directory))
8bfe682a
CD
501 (save-excursion
502 (setq buf (find-file file))
afe98dfa
CD
503 (when (and (= (point-min) (point-max)))
504 (insert "\n")
505 (save-buffer)
506 (when org-mobile-use-encryption
507 (write-file org-mobile-encryption-tempfile)
508 (org-mobile-encrypt-and-move org-mobile-encryption-tempfile file)))
8bfe682a
CD
509 (push (cons org-mobile-capture-file (md5 (buffer-string)))
510 org-mobile-checksum-files))
afe98dfa 511 (org-mobile-cleanup-encryption-tempfile)
8bfe682a 512 (kill-buffer buf)))
8d642074
CD
513
514(defun org-mobile-write-checksums ()
515 "Create checksums for all files in `org-mobile-directory'.
516The table of checksums is written to the file mobile-checksums."
8bfe682a
CD
517 (let ((sumfile (expand-file-name "checksums.dat" org-mobile-directory))
518 (files org-mobile-checksum-files)
519 entry file sum)
520 (with-temp-file sumfile
521 (set-buffer-file-coding-system 'undecided-unix nil)
522 (while (setq entry (pop files))
523 (setq file (car entry) sum (cdr entry))
524 (insert (format "%s %s\n" sum file))))))
8d642074
CD
525
526(defun org-mobile-sumo-agenda-command ()
527 "Return an agenda custom command that comprises all custom commands."
528 (let ((custom-list
529 ;; normalize different versions
530 (delq nil
531 (mapcar
532 (lambda (x)
533 (cond ((stringp (cdr x)) nil)
534 ((stringp (nth 1 x)) x)
535 ((not (nth 1 x)) (cons (car x) (cons "" (cddr x))))
536 (t (cons (car x) (cons "" (cdr x))))))
537 org-agenda-custom-commands)))
ed21c5c8
CD
538 (default-list '(("a" "Agenda" agenda) ("t" "All TODO" alltodo)))
539 thelist new e key desc type match settings cmds gkey gdesc gsettings cnt)
540 (cond
541 ((eq org-mobile-agendas 'custom)
542 (setq thelist custom-list))
543 ((eq org-mobile-agendas 'default)
544 (setq thelist default-list))
545 ((eq org-mobile-agendas 'all)
546 (setq thelist custom-list)
547 (unless (assoc "t" thelist) (push '("t" "ALL TODO" alltodo) thelist))
548 (unless (assoc "a" thelist) (push '("a" "Agenda" agenda) thelist)))
549 ((listp org-mobile-agendas)
550 (setq thelist (append custom-list default-list))
551 (setq thelist (delq nil (mapcar (lambda (k) (assoc k thelist))
552 org-mobile-agendas)))))
553 (while (setq e (pop thelist))
8d642074
CD
554 (cond
555 ((stringp (cdr e))
556 ;; this is a description entry - skip it
557 )
558 ((eq (nth 2 e) 'search)
559 ;; Search view is interactive, skip
560 )
561 ((memq (nth 2 e) '(todo-tree tags-tree occur-tree))
562 ;; These are trees, not really agenda commands
563 )
ed21c5c8
CD
564 ((and (memq (nth 2 e) '(todo tags tags-todo))
565 (or (null (nth 3 e))
566 (not (string-match "\\S-" (nth 3 e)))))
567 ;; These would be interactive because the match string is empty
568 )
569 ((memq (nth 2 e) '(agenda alltodo todo tags tags-todo))
8d642074
CD
570 ;; a normal command
571 (setq key (car e) desc (nth 1 e) type (nth 2 e) match (nth 3 e)
572 settings (nth 4 e))
573 (setq settings
574 (cons (list 'org-agenda-title-append
8bfe682a 575 (concat "<after>KEYS=" key " TITLE: "
8d642074
CD
576 (if (and (stringp desc) (> (length desc) 0))
577 desc (symbol-name type))
8bfe682a 578 " " match "</after>"))
8d642074
CD
579 settings))
580 (push (list type match settings) new))
3ab2c837
BG
581 ((or (functionp (nth 2 e)) (symbolp (nth 2 e)))
582 ;; A user-defined function, which can do anything, so simply
583 ;; ignore it.
8d642074
CD
584 )
585 (t
586 ;; a block agenda
587 (setq gkey (car e) gdesc (nth 1 e) gsettings (nth 3 e) cmds (nth 2 e))
588 (setq cnt 0)
589 (while (setq e (pop cmds))
590 (setq type (car e) match (nth 1 e) settings (nth 2 e))
591 (setq settings (append gsettings settings))
592 (setq settings
593 (cons (list 'org-agenda-title-append
8bfe682a 594 (concat "<after>KEYS=" gkey "#" (number-to-string
8d642074 595 (setq cnt (1+ cnt)))
8bfe682a 596 " TITLE: " gdesc " " match "</after>"))
8d642074
CD
597 settings))
598 (push (list type match settings) new)))))
8bfe682a
CD
599 (and new (list "X" "SUMO" (reverse new)
600 '((org-agenda-compact-blocks nil))))))
601
602(defvar org-mobile-creating-agendas nil)
603(defun org-mobile-write-agenda-for-mobile (file)
604 (let ((all (buffer-string)) in-date id pl prefix line app short m sexp)
605 (with-temp-file file
606 (org-mode)
607 (insert "#+READONLY\n")
608 (insert all)
609 (goto-char (point-min))
610 (while (not (eobp))
611 (cond
612 ((looking-at "[ \t]*$")) ; keep empty lines
613 ((looking-at "=+$")
614 ;; remove underlining
615 (delete-region (point) (point-at-eol)))
616 ((get-text-property (point) 'org-agenda-structural-header)
617 (setq in-date nil)
618 (setq app (get-text-property (point)
619 'org-agenda-title-append))
620 (setq short (get-text-property (point)
621 'short-heading))
622 (when (and short (looking-at ".+"))
623 (replace-match short)
624 (beginning-of-line 1))
625 (when app
626 (end-of-line 1)
627 (insert app)
628 (beginning-of-line 1))
629 (insert "* "))
630 ((get-text-property (point) 'org-agenda-date-header)
631 (setq in-date t)
632 (insert "** "))
633 ((setq m (or (get-text-property (point) 'org-hd-marker)
634 (get-text-property (point) 'org-marker)))
635 (setq sexp (member (get-text-property (point) 'type)
636 '("diary" "sexp")))
3ab2c837 637 (if (setq pl (text-property-any (point) (point-at-eol) 'org-heading t))
8bfe682a
CD
638 (progn
639 (setq prefix (org-trim (buffer-substring
3ab2c837 640 (point) pl))
8bfe682a 641 line (org-trim (buffer-substring
3ab2c837 642 pl
8bfe682a
CD
643 (point-at-eol))))
644 (delete-region (point-at-bol) (point-at-eol))
645 (insert line "<before>" prefix "</before>")
646 (beginning-of-line 1))
647 (and (looking-at "[ \t]+") (replace-match "")))
648 (insert (if in-date "*** " "** "))
649 (end-of-line 1)
650 (insert "\n")
651 (unless sexp
652 (insert (org-agenda-get-some-entry-text
653 m 10 " " 'planning)
654 "\n")
655 (when (setq id
656 (if (org-bound-and-true-p
657 org-mobile-force-id-on-agenda-items)
658 (org-id-get m 'create)
afe98dfa
CD
659 (or (org-entry-get m "ID")
660 (org-mobile-get-outline-path-link m))))
8bfe682a
CD
661 (insert " :PROPERTIES:\n :ORIGINAL_ID: " id
662 "\n :END:\n")))))
663 (beginning-of-line 2))
afe98dfa 664 (push (cons "agendas.org" (md5 (buffer-string)))
8bfe682a
CD
665 org-mobile-checksum-files))
666 (message "Agenda written to Org file %s" file)))
8d642074 667
afe98dfa
CD
668(defun org-mobile-get-outline-path-link (pom)
669 (org-with-point-at pom
670 (concat "olp:"
671 (org-mobile-escape-olp (file-name-nondirectory buffer-file-name))
672 "/"
673 (mapconcat 'org-mobile-escape-olp
674 (org-get-outline-path)
675 "/")
676 "/"
677 (org-mobile-escape-olp (nth 4 (org-heading-components))))))
678
679(defun org-mobile-escape-olp (s)
3ab2c837 680 (let ((table '(?: ?/)))
afe98dfa
CD
681 (org-link-escape s table)))
682
8d642074
CD
683;;;###autoload
684(defun org-mobile-create-sumo-agenda ()
685 "Create a file that contains all custom agenda views."
686 (interactive)
687 (let* ((file (expand-file-name "agendas.org"
688 org-mobile-directory))
ed21c5c8
CD
689 (file1 (if org-mobile-use-encryption
690 org-mobile-encryption-tempfile
691 file))
8bfe682a 692 (sumo (org-mobile-sumo-agenda-command))
8d642074 693 (org-agenda-custom-commands
ed21c5c8 694 (list (append sumo (list (list file1)))))
8bfe682a 695 (org-mobile-creating-agendas t))
ed21c5c8
CD
696 (unless (file-writable-p file1)
697 (error "Cannot write to file %s" file1))
8bfe682a 698 (when sumo
ed21c5c8
CD
699 (org-store-agenda-views))
700 (when org-mobile-use-encryption
afe98dfa
CD
701 (org-mobile-encrypt-and-move file1 file)
702 (delete-file file1)
703 (org-mobile-cleanup-encryption-tempfile))))
ed21c5c8
CD
704
705(defun org-mobile-encrypt-and-move (infile outfile)
706 "Encrypt INFILE locally to INFILE_enc, then move it to OUTFILE.
707We do this in two steps so that remote paths will work, even if the
708encryption program does not understand them."
709 (let ((encfile (concat infile "_enc")))
710 (org-mobile-encrypt-file infile encfile)
711 (when outfile
712 (copy-file encfile outfile 'ok-if-exists)
713 (delete-file encfile))))
714
715(defun org-mobile-encrypt-file (infile outfile)
716 "Encrypt INFILE to OUTFILE, using `org-mobile-encryption-password'."
717 (shell-command
718 (format "openssl enc -aes-256-cbc -salt -pass %s -in %s -out %s"
afe98dfa
CD
719 (shell-quote-argument (concat "pass:"
720 (org-mobile-encryption-password)))
ed21c5c8
CD
721 (shell-quote-argument (expand-file-name infile))
722 (shell-quote-argument (expand-file-name outfile)))))
723
724(defun org-mobile-decrypt-file (infile outfile)
725 "Decrypt INFILE to OUTFILE, using `org-mobile-encryption-password'."
726 (shell-command
727 (format "openssl enc -d -aes-256-cbc -salt -pass %s -in %s -out %s"
afe98dfa
CD
728 (shell-quote-argument (concat "pass:"
729 (org-mobile-encryption-password)))
ed21c5c8
CD
730 (shell-quote-argument (expand-file-name infile))
731 (shell-quote-argument (expand-file-name outfile)))))
8d642074 732
afe98dfa
CD
733(defun org-mobile-cleanup-encryption-tempfile ()
734 "Remove the encryption tempfile if it exists."
735 (and (stringp org-mobile-encryption-tempfile)
736 (file-exists-p org-mobile-encryption-tempfile)
737 (delete-file org-mobile-encryption-tempfile)))
738
8d642074
CD
739(defun org-mobile-move-capture ()
740 "Move the contents of the capture file to the inbox file.
741Return a marker to the location where the new content has been added.
8bfe682a 742If nothing new has been added, return nil."
8d642074 743 (interactive)
ed21c5c8
CD
744 (let* ((encfile nil)
745 (capture-file (expand-file-name org-mobile-capture-file
746 org-mobile-directory))
747 (inbox-buffer (find-file-noselect org-mobile-inbox-for-pull))
748 (capture-buffer
749 (if (not org-mobile-use-encryption)
750 (find-file-noselect capture-file)
afe98dfa 751 (org-mobile-cleanup-encryption-tempfile)
ed21c5c8
CD
752 (setq encfile (concat org-mobile-encryption-tempfile "_enc"))
753 (copy-file capture-file encfile)
754 (org-mobile-decrypt-file encfile org-mobile-encryption-tempfile)
755 (find-file-noselect org-mobile-encryption-tempfile)))
756 (insertion-point (make-marker))
757 not-empty content)
81ad75af 758 (with-current-buffer capture-buffer
8d642074
CD
759 (setq content (buffer-string))
760 (setq not-empty (string-match "\\S-" content))
761 (when not-empty
762 (set-buffer inbox-buffer)
763 (widen)
764 (goto-char (point-max))
765 (or (bolp) (newline))
766 (move-marker insertion-point
767 (prog1 (point) (insert content)))
768 (save-buffer)
769 (set-buffer capture-buffer)
770 (erase-buffer)
8bfe682a
CD
771 (save-buffer)
772 (org-mobile-update-checksum-for-capture-file (buffer-string))))
8d642074 773 (kill-buffer capture-buffer)
ed21c5c8
CD
774 (when org-mobile-use-encryption
775 (org-mobile-encrypt-and-move org-mobile-encryption-tempfile
afe98dfa
CD
776 capture-file)
777 (org-mobile-cleanup-encryption-tempfile))
8d642074
CD
778 (if not-empty insertion-point)))
779
8bfe682a 780(defun org-mobile-update-checksum-for-capture-file (buffer-string)
ed21c5c8 781 "Find the checksum line and modify it to match BUFFER-STRING."
8bfe682a
CD
782 (let* ((file (expand-file-name "checksums.dat" org-mobile-directory))
783 (buffer (find-file-noselect file)))
784 (when buffer
785 (with-current-buffer buffer
786 (when (re-search-forward (concat "\\([0-9a-fA-F]\\{30,\\}\\).*?"
787 (regexp-quote org-mobile-capture-file)
788 "[ \t]*$") nil t)
789 (goto-char (match-beginning 1))
790 (delete-region (match-beginning 1) (match-end 1))
791 (insert (md5 buffer-string))
792 (save-buffer)))
793 (kill-buffer buffer))))
794
795(defun org-mobile-apply (&optional beg end)
796 "Apply all change requests in the current buffer.
8d642074
CD
797If BEG and END are given, only do this in that region."
798 (interactive)
799 (require 'org-archive)
800 (setq org-mobile-last-flagged-files nil)
801 (setq beg (or beg (point-min)) end (or end (point-max)))
8bfe682a
CD
802
803 ;; Remove all Note IDs
8d642074 804 (goto-char beg)
8bfe682a
CD
805 (while (re-search-forward "^\\*\\* Note ID: [-0-9A-F]+[ \t]*\n" end t)
806 (replace-match ""))
807
808 ;; Find all the referenced entries, without making any changes yet
8d642074 809 (let ((marker (make-marker))
8bfe682a 810 (bos-marker (make-marker))
8d642074 811 (end (move-marker (make-marker) end))
8bfe682a
CD
812 (cnt-new 0)
813 (cnt-edit 0)
814 (cnt-flag 0)
815 (cnt-error 0)
816 buf-list
817 id-pos org-mobile-error)
818
819 ;; Count the new captures
820 (goto-char beg)
821 (while (re-search-forward "^\\* \\(.*\\)" end t)
822 (and (>= (- (match-end 1) (match-beginning 1)) 2)
823 (not (equal (downcase (substring (match-string 1) 0 2)) "f("))
824 (incf cnt-new)))
825
826 (goto-char beg)
8d642074 827 (while (re-search-forward
8bfe682a
CD
828 "^\\*+[ \t]+F(\\([^():\n]*\\)\\(:\\([^()\n]*\\)\\)?)[ \t]+\\[\\[\\(\\(id\\|olp\\):\\([^]\n]+\\)\\)" end t)
829 (setq id-pos (condition-case msg
830 (org-mobile-locate-entry (match-string 4))
831 (error (nth 1 msg))))
832 (when (and (markerp id-pos)
833 (not (member (marker-buffer id-pos) buf-list)))
834 (org-mobile-timestamp-buffer (marker-buffer id-pos))
835 (push (marker-buffer id-pos) buf-list))
836
837 (if (or (not id-pos) (stringp id-pos))
838 (progn
839 (goto-char (+ 2 (point-at-bol)))
840 (insert id-pos " ")
841 (incf cnt-error))
842 (add-text-properties (point-at-bol) (point-at-eol)
843 (list 'org-mobile-marker
844 (or id-pos "Linked entry not found")))))
845
846 ;; OK, now go back and start applying
847 (goto-char beg)
848 (while (re-search-forward "^\\*+[ \t]+F(\\([^():\n]*\\)\\(:\\([^()\n]*\\)\\)?)" end t)
8d642074 849 (catch 'next
8bfe682a
CD
850 (setq id-pos (get-text-property (point-at-bol) 'org-mobile-marker))
851 (if (not (markerp id-pos))
852 (progn
853 (incf cnt-error)
854 (insert "UNKNOWN PROBLEM"))
855 (let* ((action (match-string 1))
856 (data (and (match-end 3) (match-string 3)))
857 (bos (point-at-bol))
858 (eos (save-excursion (org-end-of-subtree t t)))
859 (cmd (if (equal action "")
860 '(progn
861 (incf cnt-flag)
862 (org-toggle-tag "FLAGGED" 'on)
863 (and note
864 (org-entry-put nil "THEFLAGGINGNOTE" note)))
865 (incf cnt-edit)
866 (cdr (assoc action org-mobile-action-alist))))
867 (note (and (equal action "")
868 (buffer-substring (1+ (point-at-eol)) eos)))
869 (org-inhibit-logging 'note) ;; Do not take notes interactively
870 old new)
871 (goto-char bos)
872 (move-marker bos-marker (point))
873 (if (re-search-forward "^** Old value[ \t]*$" eos t)
874 (setq old (buffer-substring
875 (1+ (match-end 0))
876 (progn (outline-next-heading) (point)))))
877 (if (re-search-forward "^** New value[ \t]*$" eos t)
878 (setq new (buffer-substring
879 (1+ (match-end 0))
880 (progn (outline-next-heading)
881 (if (eobp) (org-back-over-empty-lines))
882 (point)))))
883 (setq old (and old (if (string-match "\\S-" old) old nil)))
884 (setq new (and new (if (string-match "\\S-" new) new nil)))
885 (if (and note (> (length note) 0))
886 ;; Make Note into a single line, to fit into a property
887 (setq note (mapconcat 'identity
888 (org-split-string (org-trim note) "\n")
889 "\\n")))
890 (unless (equal data "body")
891 (setq new (and new (org-trim new))
892 old (and old (org-trim old))))
893 (goto-char (+ 2 bos-marker))
894 (unless (markerp id-pos)
895 (insert "BAD REFERENCE ")
896 (incf cnt-error)
897 (throw 'next t))
898 (unless cmd
899 (insert "BAD FLAG ")
900 (incf cnt-error)
901 (throw 'next t))
902 ;; Remember this place so that we can return
903 (move-marker marker (point))
904 (setq org-mobile-error nil)
905 (save-excursion
906 (condition-case msg
907 (org-with-point-at id-pos
908 (progn
8d642074
CD
909 (eval cmd)
910 (if (member "FLAGGED" (org-get-tags))
911 (add-to-list 'org-mobile-last-flagged-files
912 (buffer-file-name (current-buffer))))))
8bfe682a
CD
913 (error (setq org-mobile-error msg))))
914 (when org-mobile-error
3ab2c837 915 (org-pop-to-buffer-same-window (marker-buffer marker))
8bfe682a
CD
916 (goto-char marker)
917 (incf cnt-error)
918 (insert (if (stringp (nth 1 org-mobile-error))
919 (nth 1 org-mobile-error)
920 "EXECUTION FAILED")
921 " ")
922 (throw 'next t))
923 ;; If we get here, the action has been applied successfully
924 ;; So remove the entry
925 (goto-char bos-marker)
926 (delete-region (point) (org-end-of-subtree t t))))))
927 (save-buffer)
8d642074 928 (move-marker marker nil)
8bfe682a
CD
929 (move-marker end nil)
930 (message "%d new, %d edits, %d flags, %d errors" cnt-new
931 cnt-edit cnt-flag cnt-error)
932 (sit-for 1)))
933
934(defun org-mobile-timestamp-buffer (buf)
935 "Time stamp buffer BUF, just to make sure its checksum will change."
936 (with-current-buffer buf
937 (save-excursion
938 (save-restriction
939 (widen)
940 (goto-char (point-min))
941 (if (re-search-forward
942 "^\\([ \t]*\\)#\\+LAST_MOBILE_CHANGE:.*\n?" nil t)
943 (progn
944 (goto-char (match-end 1))
945 (delete-region (point) (match-end 0)))
946 (if (looking-at ".*?-\\*-.*-\\*-")
947 (forward-line 1)))
948 (insert "#+LAST_MOBILE_CHANGE: "
949 (format-time-string "%Y-%m-%d %T") "\n")))))
8d642074
CD
950
951(defun org-mobile-smart-read ()
952 "Parse the entry at point for shortcuts and expand them.
953These shortcuts are meant for fast and easy typing on the limited
954keyboards of a mobile device. Below we show a list of the shortcuts
955currently implemented.
956
957The entry is expected to contain an inactive time stamp indicating when
958the entry was created. When setting dates and
959times (for example for deadlines), the time strings are interpreted
960relative to that creation date.
8bfe682a 961Abbreviations are expected to take up entire lines, just because it is so
8d642074
CD
962easy to type RET on a mobile device. Abbreviations start with one or two
963letters, followed immediately by a dot and then additional information.
964Generally the entire shortcut line is removed after action have been taken.
965Time stamps will be constructed using `org-read-date'. So for example a
966line \"dd. 2tue\" will set a deadline on the second Tuesday after the
967creation date.
968
969Here are the shortcuts currently implemented:
970
971dd. string set deadline
972ss. string set scheduling
973tt. string set time tamp, here.
974ti. string set inactive time
975
976tg. tag1 tag2 tag3 set all these tags, change case where necessary
977td. kwd set this todo keyword, change case where necessary
978
979FIXME: Hmmm, not sure if we can make his work against the
980auto-correction feature. Needs a bit more thinking. So this function
981is currently a noop.")
982
8bfe682a
CD
983(defun org-mobile-locate-entry (link)
984 (if (string-match "\\`id:\\(.*\\)$" link)
985 (org-id-find (match-string 1 link) 'marker)
986 (if (not (string-match "\\`olp:\\(.*?\\):\\(.*\\)$" link))
987 nil
988 (let ((file (match-string 1 link))
3ab2c837
BG
989 (path (match-string 2 link)))
990 (setq file (org-link-unescape file))
8bfe682a 991 (setq file (expand-file-name file org-directory))
3ab2c837 992 (setq path (mapcar 'org-link-unescape
8bfe682a
CD
993 (org-split-string path "/")))
994 (org-find-olp (cons file path))))))
995
996(defun org-mobile-edit (what old new)
997 "Edit item WHAT in the current entry by replacing OLD with NEW.
998WHAT can be \"heading\", \"todo\", \"tags\", \"priority\", or \"body\".
999The edit only takes place if the current value is equal (except for
1000white space) the OLD. If this is so, OLD will be replace by NEW
1001and the command will return t. If something goes wrong, a string will
1002be returned that indicates what went wrong."
1003 (let (current old1 new1)
1004 (if (stringp what) (setq what (intern what)))
1005
1006 (cond
1007
1008 ((memq what '(todo todostate))
1009 (setq current (org-get-todo-state))
1010 (cond
1011 ((equal new "DONEARCHIVE")
1012 (org-todo 'done)
1013 (org-archive-subtree-default))
1014 ((equal new current) t) ; nothing needs to be done
1015 ((or (equal current old)
1016 (eq org-mobile-force-mobile-change t)
1017 (memq 'todo org-mobile-force-mobile-change))
1018 (org-todo (or new 'none)) t)
1019 (t (error "State before change was expected as \"%s\", but is \"%s\""
1020 old current))))
ed21c5c8 1021
8bfe682a
CD
1022 ((eq what 'tags)
1023 (setq current (org-get-tags)
1024 new1 (and new (org-split-string new ":+"))
1025 old1 (and old (org-split-string old ":+")))
1026 (cond
1027 ((org-mobile-tags-same-p current new1) t) ; no change needed
1028 ((or (org-mobile-tags-same-p current old1)
1029 (eq org-mobile-force-mobile-change t)
1030 (memq 'tags org-mobile-force-mobile-change))
1031 (org-set-tags-to new1) t)
1032 (t (error "Tags before change were expected as \"%s\", but are \"%s\""
1033 (or old "") (or current "")))))
ed21c5c8 1034
8bfe682a
CD
1035 ((eq what 'priority)
1036 (when (looking-at org-complex-heading-regexp)
1037 (setq current (and (match-end 3) (substring (match-string 3) 2 3)))
1038 (cond
1039 ((equal current new) t) ; no action required
1040 ((or (equal current old)
1041 (eq org-mobile-force-mobile-change t)
1042 (memq 'tags org-mobile-force-mobile-change))
1043 (org-priority (and new (string-to-char new))))
1044 (t (error "Priority was expected to be %s, but is %s"
1045 old current)))))
1046
1047 ((eq what 'heading)
1048 (when (looking-at org-complex-heading-regexp)
1049 (setq current (match-string 4))
1050 (cond
1051 ((equal current new) t) ; no action required
1052 ((or (equal current old)
1053 (eq org-mobile-force-mobile-change t)
1054 (memq 'heading org-mobile-force-mobile-change))
1055 (goto-char (match-beginning 4))
1056 (insert new)
1057 (delete-region (point) (+ (point) (length current)))
1058 (org-set-tags nil 'align))
1059 (t (error "Heading changed in MobileOrg and on the computer")))))
ed21c5c8 1060
8bfe682a
CD
1061 ((eq what 'body)
1062 (setq current (buffer-substring (min (1+ (point-at-eol)) (point-max))
1063 (save-excursion (outline-next-heading)
1064 (point))))
1065 (if (not (string-match "\\S-" current)) (setq current nil))
1066 (cond
1067 ((org-mobile-bodies-same-p current new) t) ; no action necessary
1068 ((or (org-mobile-bodies-same-p current old)
1069 (eq org-mobile-force-mobile-change t)
1070 (memq 'body org-mobile-force-mobile-change))
1071 (save-excursion
1072 (end-of-line 1)
1073 (insert "\n" new)
1074 (or (bolp) (insert "\n"))
1075 (delete-region (point) (progn (org-back-to-heading t)
1076 (outline-next-heading)
1077 (point))))
1078 t)
1079 (t (error "Body was changed in MobileOrg and on the computer")))))))
ed21c5c8 1080
8bfe682a
CD
1081(defun org-mobile-tags-same-p (list1 list2)
1082 "Are the two tag lists the same?"
1083 (not (or (org-delete-all list1 list2)
1084 (org-delete-all list2 list1))))
1085
1086(defun org-mobile-bodies-same-p (a b)
1087 "Compare if A and B are visually equal strings.
1088We first remove leading and trailing white space from the entire strings.
1089Then we split the strings into lines and remove leading/trailing whitespace
1090from each line. Then we compare.
1091A and B must be strings or nil."
1092 (cond
1093 ((and (not a) (not b)) t)
1094 ((or (not a) (not b)) nil)
1095 (t (setq a (org-trim a) b (org-trim b))
1096 (setq a (mapconcat 'identity (org-split-string a "[ \t]*\n[ \t]*") "\n"))
1097 (setq b (mapconcat 'identity (org-split-string b "[ \t]*\n[ \t]*") "\n"))
1098 (equal a b))))
1099
8d642074
CD
1100(provide 'org-mobile)
1101
5b409b39 1102
8d642074
CD
1103
1104;;; org-mobile.el ends here
1105