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