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