(add-to-list 'load-path "~/src/emacs/org-mode/lisp/")
(add-to-list 'load-path "~/src/emacs/org-mode/contrib/lisp/" t)
(add-to-list 'Info-directory-list "~/src/emacs/org-mode/doc/")
(setq org-modules '(org-bibtex org-gnus org-info))
(setq org-log-done t
org-log-into-drawer t
org-clock-into-drawer t
org-todo-keywords '((sequence "TODO(t)" "STARTED(s)" "WAITING(w@)"
"|" "DONE(d)" "NA(n@)")))
(setq org-catch-invisible-edits 'error
org-special-ctrl-k t
org-insert-heading-respect-content t
org-M-RET-may-split-line nil
org-adapt-indentation nil
org-blank-before-new-entry '((heading . t) (plain-list-item . auto)))
(setq org-link-search-must-match-exact-headline nil)
(setq org-use-speed-commands t
org-use-extra-keys t
org-fast-tag-selection-single-key 'expert)
(setq org-completion-use-ido t
org-outline-path-complete-in-steps nil
org-goto-interface 'outline-path-completionp
org-goto-max-level 3)
(put 'org-goto-max-level 'safe-local-variable #'integerp)
(setq org-structure-template-alist
'(("p" "#+property: " "")
("o" "#+options: " "")
("d" "#+date: " "")
("t" "#+title: " "")
("S" "#+setupfile: ?" "")
("n" "#+begin_note\n ?\n#+end_note" "\n?\n")
("w" "#+begin_note\n ?\n#+end_note" "\n?\n")
("C" "#+caption: " "")
("b" "#+label: " "")
("r" "#+attr_latex: " "")
("R" "#+attr_html: " "")
;; Lower case versions of defaults
("s" "#+begin_src ?\n \n#+end_src" "\n\n")
("e" "#+begin_example\n ?\n#+end_example" "\n?\n")
("q" "#+begin_quote\n ?\n#+end_quote" "\n?\n
")
("v" "#+begin_versen ?\n#+end_verse" "\n?\n")
("V" "#+begin_verbatim\n ?\n#+end_verbatim" "\n?\n")
("c" "#+begin_center\n ?\n#+end_center" "
\n?\n")
("l" "#+begin_latex\n ?\n#+end_latex" "\n?\n")
("L" "#+latex: " "?")
("h" "#+begin_html\n ?\n#+end_html" "\n?\n")
("H" "#+html: " "?")
("a" "#+begin_ascii\n ?\n#+end_ascii" "")
("A" "#+ascii: " "")
("i" "#+index: ?" "#+index: ?")
("I" "#+include: %file ?" "")))
(add-to-list 'auto-mode-alist '("\\.org.txt\\'" . org-mode))
(defvar km/org-store-link-hook nil
"Hook run before by `km/org-store-link-hook'.
These are run within a `save-window-excursion' block.")
(defun km/org-store-link ()
"Run `km/org-store-link-hook' before `org-store-link'.
The hook functions and `org-store-link' are called within a
`save-window-excursion' block."
(interactive)
(save-window-excursion
(run-hooks 'km/org-store-link-hook)
(call-interactively 'org-store-link)))
(defun km/org-tree-to-indirect-buffer (&optional arg)
"Run `org-tree-to-indirect-buffer', keeping previous buffer.
By default, `org-tree-to-indirect-buffer' deletes the previous
indirect buffer when making a new one to avoid accumulating
buffers, which can be overriden by a C-u prefix. Reverse this
behavior so that the prefix must be given in order to delete the
previous indirect buffer. If the argument is a number, which has
a different meaning, it is left untouched."
(interactive "P")
(unless (numberp arg)
(setq arg (not arg)))
(org-tree-to-indirect-buffer arg))
(defun km/org-tree-to-indirect-buffer-current-window (&optional arg)
"Create indirect buffer and narrow to subtree in this window.
Before running `org-tree-to-indirect-buffer', set
`org-indirect-buffer-display' to `current-window'."
(interactive "P")
(let ((org-indirect-buffer-display 'current-window))
(km/org-tree-to-indirect-buffer arg)))
(defun km/org-clone-and-shift-by-repeater ()
"Clone current subtree, shifting new timestamp by repeater.
The repeater is removed from the original subtree."
(interactive)
(save-excursion
(org-back-to-heading)
(let* ((heading (org-element-at-point))
(tree-begin (org-element-property :begin heading))
(tree-end (org-element-property :end heading))
(tree (buffer-substring tree-begin tree-end))
(deadline (org-element-property :deadline heading))
(deadline-end (org-element-property :end deadline))
(repeat-val (org-element-property :repeater-value deadline))
(repeat-unit (org-element-property :repeater-unit deadline))
new-tree)
(cond
((not deadline) (user-error "Heading doesn't have deadline"))
((not repeat-val) (user-error "Deadline isn't repeating"))
((eq repeat-unit 'week)
;; `org-timestamp-change' doesn't recognize weeks.
(setq repeat-val (* repeat-val 7)
repeat-unit 'day)))
;; Make new tree with repeater shifted one cycle.
(with-temp-buffer
(insert tree)
(goto-char (point-min))
(re-search-forward org-ts-regexp)
(org-timestamp-change repeat-val repeat-unit)
(setq new-tree (buffer-string)))
;; Insert new tree with shifted repeater.
(goto-char tree-end)
(insert new-tree)
;; Remove the repeater from the original tree.
(goto-char tree-begin)
(re-search-forward
;; Regexp taken from `org-clone-subtree-with-time-shift'.
"<[^<>\n]+\\( +[.+]?\\+[0-9]+[hdwmy]\\)" deadline-end)
(delete-region (match-beginning 1) (match-end 1)))))
(defun km/org-sort-parent (arg)
"Sort on parent heading ARG levels up.
After sorting, return point to its previous location under the
current heading."
(interactive "p")
(let ((heading (org-no-properties (org-get-heading t t)))
(starting-pos (point))
chars-after-heading)
(org-back-to-heading t)
(setq chars-after-heading (- starting-pos (point)))
(outline-up-heading arg)
(call-interactively #'org-sort)
;; Sorting doesn't play well with `save-restriction' or markers,
;; so just put the point where it was relative to the original
;; heading. This may not actually be the same tree if there are
;; redundant headings.
(goto-char (+ (org-find-exact-headline-in-buffer heading nil t)
chars-after-heading))))
(defun km/org-sort-heading-ignoring-articles ()
"Sort alphabetically, but ignore any leading articles."
(let* ((ignored-words '("a" "an" "the"))
(heading (org-no-properties
(org-get-heading 'no-tags 'no-todo)))
(heading-words (split-string heading)))
(when (member (downcase (car heading-words))
ignored-words)
(setq heading-words (cdr heading-words)))
(mapconcat #'identity heading-words " ")))
(defun km/org-remove-title-leader ()
"Remove leader from Org heading title.
Convert
* TODO leader: Rest of title :tag:
to
* TODO Rest of title :tag:"
(interactive)
(save-excursion
(let ((regex (format "^%s\\(?:%s \\)?\\(?:%s \\)?\\(.*: \\)\\w+"
org-outline-regexp org-todo-regexp
org-priority-regexp)))
(org-back-to-heading)
(when (re-search-forward regex (point-at-eol) t)
(replace-match "" nil nil nil 4)
(org-set-tags nil t)))))
(defun km/org-add-blank-before-heading ()
"Add a blank line before Org headings in buffer."
(interactive)
(save-excursion
(goto-char (point-min))
(while (re-search-forward "[^\n]\n\\*" nil t)
(when (org-at-heading-p)
(beginning-of-line)
(open-line 1)))))
(defun km/org-normalize-spaces ()
"Reduce to single spaces and add space before headings."
(interactive)
(km/reduce-to-single-spaces)
(km/org-add-blank-before-heading))
(defun km/org-switch-to-buffer-other-window (&optional arg)
(interactive "P")
(noflet ((org-pop-to-buffer-same-window (&optional buffer-or-name norecord label)
(funcall 'pop-to-buffer buffer-or-name nil norecord)))
(org-switchb arg)))
(after 'org
(define-key org-mode-map (kbd "C-c C-x B")
'km/org-tree-to-indirect-buffer-current-window)
(define-key org-mode-map [remap org-tree-to-indirect-buffer]
'km/org-tree-to-indirect-buffer)
;; Rebind `org-insert-drawer' to so that `org-metadown' has the
;; expected "C-c C-x" keybinding.
(define-key org-mode-map (kbd "C-c C-x d") 'org-metadown)
(define-key org-mode-map (kbd "C-c C-x w") 'org-insert-drawer)
;; Rebind `org-set-property' to free up binding for
;; `org-previous-item'.
(define-key org-mode-map (kbd "C-c C-x s") 'org-set-property)
(define-key org-mode-map (kbd "C-c C-x n") 'org-next-item)
(define-key org-mode-map (kbd "C-c C-x p") 'org-previous-item)
;; Override global `imenu' binding.
(define-key org-mode-map (kbd "C-c j") 'org-goto)
;; Don't let `org-cycle-agenda-files' binding override custom
;; `backward-kill-word' binding (`org-cycle-agenda-files' is still bound
;; to C-,).
(define-key org-mode-map (kbd "C-'") nil)
(define-key org-mode-map (kbd "C-c m") 'km/org-prefix-map))
(define-prefix-command 'km/org-prefix-map)
(define-key km/org-prefix-map "c" 'km/org-clone-and-shift-by-repeater)
(define-key km/org-prefix-map "l" 'km/org-remove-title-leader)
(define-key km/org-prefix-map "n" 'km/org-normalize-spaces)
(define-key km/org-prefix-map "s" 'km/org-sort-parent)
(define-prefix-command 'km/global-org-map)
(global-set-key (kbd "C-c o") 'km/global-org-map)
(define-key km/global-org-map "b" 'org-iswitchb)
(define-key km/global-org-map "l" 'km/org-store-link)
(define-key km/global-org-map "o" 'org-open-at-point-global)
(define-key km/global-org-map "s" 'org-save-all-org-buffers)
(define-key ctl-x-4-map "o" 'km/org-switch-to-buffer-other-window)
;;; Agenda
(setq org-default-notes-file "~/notes/agenda/tasks.org")
(defvar km/org-agenda-file-directory "~/notes/agenda/")
(setq org-agenda-files (list km/org-agenda-file-directory))
(setq org-agenda-text-search-extra-files
(file-expand-wildcards "~/notes/extra/*.org"))
(setq org-agenda-restore-windows-after-quit t
org-agenda-window-setup 'only-window
org-agenda-sticky t)
(setq org-agenda-dim-blocked-tasks nil
org-agenda-show-all-dates t
org-agenda-skip-deadline-if-done t
org-agenda-skip-scheduled-if-done t
org-agenda-start-on-weekday nil
org-agenda-use-time-grid nil)
(setq org-agenda-sorting-strategy
'((agenda time-up deadline-up scheduled-up priority-down category-keep)
(todo priority-down category-keep)
(tags priority-down category-keep)
(search category-keep)))
(setq org-agenda-custom-commands
'(("d" todo "DONE" nil)
("u" "Unschedule TODO entries" alltodo ""
((org-agenda-skip-function
(lambda nil
(org-agenda-skip-entry-if 'scheduled 'deadline
'regexp "\n]+>")))
(org-agenda-overriding-header "Unscheduled TODO entries: ")))
("p" "Past timestamps" tags "TIMESTAMP<=\"\"")))
(setq org-capture-templates
'(("t" "task" entry (file+headline "~/notes/tasks.org" "Inbox")
"* TODO %?\n%i")
("d" "date" entry (file+headline "~/notes/calendar.org" "Inbox")
"* %?\n%i")
("m" "misc" entry (file+headline "~/notes/misc.org" "Inbox")
"* %?\n%i")
;; Link counterparts
("T" "task link" entry (file+headline "~/notes/tasks.org" "Inbox")
"* TODO %?\n%i\nLink: %a")
("D" "date link" entry (file+headline "~/notes/calendar.org" "Inbox")
"* %?\n%i\nLink: %a")
("M" "misc link" entry (file+headline "~/notes/misc.org" "Inbox")
"* %?\n%i\nLink: %a")
;; Clipboard
("x" "task clipboard" entry (file+headline "~/notes/tasks.org" "Inbox")
"* TODO %?\n%x")
("X" "misc clipboard" entry (file+headline "~/notes/misc.org" "Inbox")
"* %?\n%x")))
(add-hook 'org-agenda-mode-hook 'km/org-agenda-cd-and-read-dir-locals)
(add-hook 'org-agenda-finalize-hook 'km/org-agenda-store-current-span)
(defun km/org-agenda-cd-and-read-dir-locals ()
(setq default-directory "~/notes/")
(hack-local-variables))
(defun km/org-agenda-store-current-span ()
"Store the current span value in `org-agenda-span'.
This allows the view to persist when the agenda buffer is
killed."
(when org-agenda-current-span
(setq org-agenda-span org-agenda-current-span)))
(defun km/org-agenda-add-or-remove-file (file)
"Add or remove link to FILE in `km/org-agenda-file-directory'.
If a link for FILE does not exist, create it. Otherwise, remove
it. Like `org-agenda-file-to-front', this results in FILE being
displayed in the agenda."
(interactive (list (cl-case major-mode
(org-mode (buffer-file-name))
(dired-mode (dired-get-filename))
(org-agenda-mode (ignore-errors (save-window-excursion
(org-agenda-goto)
(buffer-file-name))))
(t (read-file-name "Link file: ")))))
(let ((agenda-file (expand-file-name (file-name-nondirectory file)
km/org-agenda-file-directory)))
(if (file-equal-p (file-truename agenda-file) file)
(progn
(when (called-interactively-p) (message "Deleting %s" agenda-file))
(delete-file agenda-file))
(when (called-interactively-p) (message "Adding %s" agenda-file))
(make-symbolic-link file agenda-file))))
(defun km/org-open-default-notes-file-inbox ()
"Open \"Inbox\" heading of `org-default-notes-file'."
(interactive)
(find-file org-default-notes-file)
(goto-char (org-find-exact-headline-in-buffer "Inbox" nil t))
(recenter-top-bottom 0)
(show-children))
(defun km/org-goto-agenda-heading ()
"Jump to heading in agenda files."
(interactive)
(let ((org-refile-targets
'((org-agenda-files :maxlevel . 3)
(org-agenda-text-search-extra-files :maxlevel . 3))))
(org-refile '(4))))
(define-key km/global-org-map "a" 'org-agenda)
(define-key km/global-org-map "c" 'org-capture)
(define-key km/global-org-map "j" 'km/org-goto-agenda-heading)
(define-key km/global-org-map "m" 'km/org-open-default-notes-file-inbox)
(define-key km/global-org-map "n" 'km/org-agenda-add-or-remove-file)
(after 'org-agenda
;; Bind `org-agenda-follow-mode' to same key as
;; `next-error-follow-minor-mode'.
(define-key org-agenda-mode-map (kbd "C-c C-f") 'org-agenda-follow-mode))
;;; Refiling
(setq org-reverse-note-order t)
(setq org-refile-target-verify-function 'km/org-refile-verify-target)
(setq org-refile-targets '((nil :maxlevel . 2))
org-refile-cache nil)
(defvar km/org-agenda-refile-targets
'((nil :maxlevel . 3)
(org-agenda-files :maxlevel . 2)
(org-agenda-text-search-extra-files :maxlevel . 2)))
(add-to-list 'safe-local-variable-values
(cons 'org-refile-targets km/org-agenda-refile-targets))
(defun km/org-refile-verify-target ()
"Exclude DONE state from refile targets."
(not (member (nth 2 (org-heading-components)) org-done-keywords)))
(defadvice org-refile (around km/org-refile-dwim activate)
"Rebind `org-refile-targets' if next window is an Org buffer.
A target is determined by `km/org-refile-dwim-target-file'."
(let* ((dwim-target (km/org-refile-dwim-target-file))
(org-refile-targets (if dwim-target
`((nil
:maxlevel . ,km/org-refile-dwim-maxlevel)
(dwim-target
:maxlevel . ,km/org-refile-dwim-maxlevel))
org-refile-targets)))
ad-do-it))
(defun km/org-refile-dwim-target-file ()
"Return next window that is an Org buffer."
(let ((from-buffer (current-buffer)))
(--when-let (get-window-with-predicate
(lambda (w)
(with-current-buffer (window-buffer w)
(and (eq major-mode 'org-mode)
(not (eq from-buffer (current-buffer)))))))
(buffer-file-name (window-buffer it)))))
(defvar km/org-refile-dwim-maxlevel 2)
(defun km/org-refile-to-other-file (file &optional maxlevel)
"Refile with `org-refile-targets' set to FILE.
A numeric prefix sets MAXLEVEL (defaults to 2)."
(interactive "fFile: \nP")
(let* ((maxlevel (prefix-numeric-value (or maxlevel 2)))
(file (substring-no-properties file))
(org-refile-targets `((,file :maxlevel . ,maxlevel))))
(org-refile)))
(defun km/org-refile-to-other-org-buffer (buffer &optional maxlevel)
"Refile with `org-refile-targets' set to BUFFER file name.
A numeric prefix sets MAXLEVEL (defaults to 2)."
(interactive (list (km/get-org-file-buffer) current-prefix-arg))
(km/org-refile-to-other-file (buffer-file-name buffer)
maxlevel))
(defun km/get-org-file-buffer ()
(get-buffer
(org-icompleting-read "Buffer: " (mapcar 'buffer-name
(org-buffer-list 'files)))))
(defun km/org-set-refiling-buffer (&optional maxlevel)
"Choose buffer to set as sole target in `org-refile-targets'.
If `org-refile-targets' is already a local variable, restore the
global value. A numeric prefix sets MAXLEVEL (defaults to 2)."
(interactive "P")
(if (local-variable-p 'org-refile-targets)
(kill-local-variable 'org-refile-targets)
(let ((buffer-file (substring-no-properties
(buffer-file-name (km/get-org-file-buffer))))
(maxlevel (prefix-numeric-value (or maxlevel 2))))
(set (make-local-variable 'org-refile-targets)
`((,buffer-file :maxlevel . ,maxlevel))))))
(define-key km/global-org-map "w" 'org-refile-goto-last-stored)
(define-key km/org-prefix-map "w" 'km/org-refile-to-other-org-buffer)
(after 'org-agenda
;; Free up 'j' for `ace-jump-mode'.
(define-key org-agenda-mode-map (kbd "C-j") 'org-agenda-goto-date)
(define-key org-agenda-mode-map "j" 'ace-jump-mode))
;;; Export
(after 'org
;; Avoid conflict when amsmath is loaded.
(setcar (rassoc '("wasysym" t) org-latex-default-packages-alist)
"nointegrals")
(add-to-list 'org-latex-packages-alist '("" "amsmath" t)))
(after 'ox-latex
(add-to-list 'org-latex-classes
'("short"
"\\documentclass{short}"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}"))))
;;; Org Babel
(setq org-confirm-babel-evaluate nil
org-src-fontify-natively t)
(org-babel-do-load-languages
'org-babel-load-languages
'((sh . t)
(python . t)
(R . t)
(emacs-lisp . t)
(latex . t)))
;;; Org Contacts
(require 'org-contacts)
(setq org-contacts-files '("~/notes/contacts.org"))
(add-to-list 'org-capture-templates
'("a" "email address" entry (file+headline "~/notes/contacts.org" "Inbox")
"** %(org-contacts-template-name)\n :PROPERTIES:\n :EMAIL: %(org-contacts-template-email)\n :END:"))
;;; Org in other modes
(after 'git-commit
(add-hook 'git-commit-setup-hook 'km/load-orgstruct))
(add-hook 'next-error-hook (lambda ()
(when (eq major-mode 'org-mode)
(org-show-context))))
(add-hook 'message-mode-hook 'km/load-orgstruct)
(defun km/load-orgstruct ()
(turn-on-orgstruct++)
(turn-on-orgtbl))
(after 'poporg
(define-key poporg-mode-map (kbd "C-c C-c") 'poporg-edit-exit))
(define-key km/global-org-map "p" 'poporg-dwim)
;;; Org open file
(defadvice org-open-file (after km/org-open-add-to-recentf activate)
(recentf-add-file path))
(defun km/org-open-file-at-point ()
"Open file at point with `org-open-file'."
(interactive)
(let ((file (thing-at-point 'filename)))
(if (and file (file-exists-p file))
(org-open-file file)
(user-error "No file at point"))))
(defun km/org-open-file ()
"Interactive version of `org-open-file'."
(interactive)
(org-open-file (read-file-name "Open file: " nil nil t)))
(autoload 'magit-annex-present-files "magit-annex")
(defun km/org-open-annex-file ()
"Open a git annex file with `org-open-file'."
(interactive)
(--if-let (magit-annex-present-files)
(org-open-file (magit-completing-read "Open annex file" it nil t))
(message "No annex files found")))
(defun km/org-open-recent-file ()
"Open a file from `recentf-list' with `org-open-file'."
(interactive)
(org-open-file (km/read-recent-file)))
(after 'init-files
(define-key km/file-map "a" 'km/org-open-annex-file)
(define-key km/file-map "o" 'km/org-open-file)
(define-key km/file-map "p" 'km/org-open-file-at-point)
(define-key km/file-map "r" 'km/org-open-recent-file))
(provide 'init-org)