(add-to-list 'load-path "~/elisp/3rd-party-lib/org-mode/lisp") (add-to-list 'load-path "~/elisp/3rd-party-lib/org-mode/contrib/lisp") (require-maybe 'htmlize) (require 'org) (require 'org-clock) (require 'org-id) ; C-c l creates CUSTOM_ID (require-maybe 'org-protocol) (require-maybe 'org-panel) (add-to-list 'auto-mode-alist '("\\.org$" . org-mode)) (define-key global-map "\C-cl" 'org-store-link) (define-key global-map "\C-ca" 'org-agenda) (add-hook 'org-mode-hook (lambda () (auto-fill-mode 1))) (defun wl-turn-on-orgstruct-mode () (orgstruct-mode 1)) (defun wl-turn-on-orgstruct++-mode () (orgstruct++-mode 1)) (defun wl-turn-on-orgtbl-mode () (orgtbl-mode 1)) (defconst wl-gtd-workflow-version "0" "GTD work flow version number, start from 0.") (defun wl-gtd-workflow-version () (interactive) (message "GTD work flow version %s" wl-gtd-workflow-version)) (setq org-todo-keywords '((sequence "TODO" "STARTED" "|" "DONE") (sequence "|" "CANCELED") (sequence "WAITING" "|" "GOT") (sequence "PROJECT" "|" "CLOSE") (sequence "OPEN@BUG" "INVESTIGATE@BUG" "FIX@BUG" "TEST@BUG" "REVIEW@BUG" "CLOSE@BUG" "|" "FIXED@BUG" "TRANSFERRED@BUG") (sequence "TOREAD" "READING" "|" "READ"))) (setq org-todo-state-tags-triggers '(("STARTED" ("NEXT" . nil) ("someday" . nil)) ("READING" ("NEXT" . nil) ("someday" . nil)) ("INVESTIGATE@BUG" ("NEXT" . nil) ("someday" . nil)) (done ("NEXT" . nil) ("someday" . nil)) ("WAITING" ("NEXT" . t)))) (setq org-todo-keyword-faces '(("WAITING" . (:foreground "gray" :weight bold)))) (setq org-stuck-projects '("+LEVEL=1|+LEVEL=2/+PROJECT" ("TODO" "STARTED" "TOREAD" "READING" "WAITING") nil "")) (add-to-list 'org-global-properties '("Effort_ALL" . "0:10 0:30 1:00 2:00 3:00 4:00 5:00 6:00")) (setq org-columns-default-format "%TODO %40ITEM %Effort{:} %CLOCKSUM{:} %TAGS") (defun wl-org-column-view-uses-fixed-width-face () ;; copy from org-faces.el (when (fboundp 'set-face-attribute) ;; Make sure that a fixed-width face is used when we have a column table. (set-face-attribute 'org-column nil :height (face-attribute 'default :height) :family (face-attribute 'default :family)))) (when (and (fboundp 'daemonp) (daemonp)) (add-hook 'org-mode-hook 'wl-org-column-view-uses-fixed-width-face)) (setq org-agenda-include-diary t) (defun wl-org-agenda-to-appt () ;; Dangerous!!! This might remove entries added by `appt-add' manually. (org-agenda-to-appt t "TODO")) (wl-org-agenda-to-appt) (add-hook 'org-finalize-agenda-hook 'wl-org-agenda-to-appt) (add-hook 'org-finalize-agenda-hook 'delete-other-windows) ;;; %.30s make `format' fail on some subject in Chinese (setq org-email-link-description-format "Email %c: %s") (setq org-icalendar-include-todo t) (setq org-insert-mode-line-in-empty-file t) (setq org-log-done 'note) (setq org-reverse-note-order t) (setq org-clock-persist t) (org-clock-persistence-insinuate) (setq org-clock-history-length 15) (setq org-completion-use-iswitchb t) (setq org-clock-modeline-total 'current) (setq org-agenda-clockreport-parameter-plist '(:link t :maxlevel 3)) (setq org-deadline-warning-days 14) ; do not worry too much ;;; modify from `org-agenda-clock-goto' (defun wl-org-agenda-clock-goto () (interactive) (let (pos) (mapc (lambda (o) (if (eq (overlay-get o 'type) 'org-agenda-clocking) (setq pos (overlay-start o)))) (overlays-in (point-min) (point-max))) (cond (pos (goto-char pos)) (org-clock-current-task (org-clock-goto)) (t (error "No running clock"))))) (defun wl-org-add-note-or-done (&optional arg) "Add note to current clocked task from anywhere. With ARG, close it. Or current task if not clocking" (interactive "p") ;; Go to clocking task if any (when (org-clocking-p) (if (eq major-mode 'org-agenda-mode) ;; FIXME: Cursor doesn't move if clocking task is not in ;; current agenda buffer. So current task is closed ;; incorrectly. (wl-org-agenda-clock-goto) (org-clock-goto))) ;; add note or mark it as done. (if (= 4 arg) (if (eq major-mode 'org-agenda-mode) (org-agenda-todo 'done) (org-todo 'done)) (if (eq major-mode 'org-agenda-mode) (org-agenda-add-note) (org-add-note))) (org-finalize-agenda)) (global-set-key (kbd "") 'wl-org-add-note-or-done) (defun wl-customize-org-agenda-custom-commands () (interactive) (unless (file-exists-p "~/gtd/") (error "gtd files not found.")) (setq org-agenda-custom-commands '(("g" "Calendar" agenda "" ((org-agenda-ndays 1) (org-deadline-warning-days 7))) ("P" "Today" ((tags-todo "-someday/STARTED|READING|OPEN@BUG|INVESTIGATE@BUG|FIX@BUG|TEST@BUG|REVIEW@BUG|CLOSE@BUG" ((org-agenda-overriding-header "Pomodoro tasks"))) (agenda "" ((org-agenda-ndays 1) (org-deadline-warning-days 7))) (tags-todo "+NEXT/TODO|TOREAD" ((org-agenda-overriding-header "Next actions"))))) ("p" "Pomodoro" ((tags-todo "+work-someday/STARTED|READING|OPEN@BUG|INVESTIGATE@BUG|FIX@BUG|TEST@BUG|REVIEW@BUG|CLOSE@BUG" ((org-agenda-overriding-header "Work tasks"))) (tags-todo "-work/STARTED|READING" ((org-agenda-overriding-header "Non-work tasks"))))) ("R" . "Roadmap") ("Rr" "Big picture" ((tags-todo "+NEXT+CATEGORY=\"baby\"|+someday+CATEGORY=\"baby\"" ((org-agenda-overriding-header "baby"))) (tags-todo "+NEXT+CATEGORY=\"life\"|+someday+CATEGORY=\"life\"" ((org-agenda-overriding-header "life"))) (tags-todo "+NEXT+CATEGORY=\"career\"|+someday+CATEGORY=\"career\"" ((org-agenda-overriding-header "career"))) (tags-todo "+NEXT+CATEGORY=\"work\"|+someday+CATEGORY=\"work\"" ((org-agenda-overriding-header "work"))) (tags-todo "-someday/PROJECT") (tags-todo "+someday/PROJECT"))) ("Rl" "Learning" ((tags-todo "book/TOREAD|book/TOREAD" ((org-agenda-overriding-header "book"))) (tags-todo "kindle/TOREAD|kindle/TOREAD" ((org-agenda-overriding-header "kindle"))) (tags-todo "video/TOREAD|video/TOREAD" ((org-agenda-overriding-header "video"))) (tags-todo "www/TOREAD|www/TOREAD" ((org-agenda-overriding-header "www"))) (tags-todo "paper/TOREAD|paper/TOREAD" ((org-agenda-overriding-header "paper"))) (tags-todo "magazine/TOREAD|magazine/TOREAD" ((org-agenda-overriding-header "magazine"))) (tags-todo "podcast/TOREAD|podcast/TOREAD" ((org-agenda-overriding-header "podcast"))))) ;; The following agenda view can not be merged together. ("Rs" "Recent (6 months) Someday" ((tags-todo "+someday+CREATED>=\"<-6m>\"+TODO=\"TODO\"" ((org-agenda-overriding-header "Recent (6 months) Someday TODO"))) (tags-todo "+someday+CREATED>=\"<-6m>\"+TODO=\"TOREAD\"" ((org-agenda-overriding-header "Recent (6 months) Someday TOREAD"))))) ("RS" "Too Old Someday" ((tags-todo "+someday-CREATED>=\"<-6m>\"+TODO=\"TODO\"" ((org-agenda-overriding-header "Too Old Someday TODO"))) (tags-todo "+someday-CREATED>=\"<-6m>\"+TODO=\"TOREAD\"" ((org-agenda-overriding-header "Too Old Someday TOREAD"))))) ("Q" . "Custom queries") ("Qn" "List learning notes" ((tags "+notes" ((org-use-tag-inheritance nil) (org-agenda-files (file-expand-wildcards (expand-file-name "*.org" org-directory))))))) ("Qm" "Search notes" (lambda (match) (org-search-view nil)) "" ((org-agenda-filter-preset '("+notes")) (org-agenda-files (file-expand-wildcards (expand-file-name "*.org" org-directory))))) ("QM" "Search tags with archives" (lambda (match) (org-tags-view nil)) "" ((org-agenda-files (append org-agenda-files (file-expand-wildcards (expand-file-name "archive-*.org" org-directory)))))) ("Qc" "Search code comment" (lambda (match) (org-search-view nil)) "" ((org-agenda-files (cons wl-org-code-file (wl-org-code-comment-files))) (org-agenda-text-search-extra-files nil))) ("Qd" "List code comment" ((tags "+LEVEL=2" ((org-agenda-files (cons wl-org-code-file (wl-org-code-comment-files))) (org-agenda-overriding-header "code comment")))))))) (when (file-exists-p "~/gtd/") (setq org-directory "~/gtd/") ;; helper functions (defun wl-expand-org-file (file &optional directory) (let ((dir (or directory org-directory))) (expand-file-name file dir))) (defun wl-expand-org-file-list (file-list &optional directory) (mapcar '(lambda (file) (wl-expand-org-file file directory)) file-list)) ;; org files (defconst wl-org-son-file (wl-expand-org-file "son.org")) (defconst wl-org-gtd-file (wl-expand-org-file "gtd.org")) (defconst wl-org-work-file (wl-expand-org-file "work.org")) (defconst wl-org-todo-file (wl-expand-org-file "todo.org")) (defconst wl-org-notes-file (wl-expand-org-file "notes.org")) (defconst wl-org-misc-file (wl-expand-org-file "misc.org")) (defconst wl-org-refs-file (wl-expand-org-file "refs.org")) (defconst wl-org-code-file (wl-expand-org-file "code.org")) (setq org-refile-targets '((wl-org-code-comment-files . (:level . 1)) (nil . (:level . 1)))) (wl-customize-org-agenda-custom-commands) ;; org-capture (setq org-capture-templates `(("e" "Email" entry (file+headline ,wl-org-work-file "Inbox") "* TODO %?\n :PROPERTIES:\n :LOGGING: nil\n :CREATED: %u\n :END:\n\n %a" :prepend t) ("w" "Work" entry (file+headline ,wl-org-work-file "Inbox") "* TODO %?\n :PROPERTIES:\n :CREATED: %u\n :END:\n\n %i\n" :prepend t) ("l" "Link" entry (file+headline ,wl-org-todo-file "Tasks") "* TODO %?\n :PROPERTIES:\n :CREATED: %u\n :END:\n\n %a" :prepend t) ("p" "Creating project") ("pt" "Project for todo" entry (file+headline ,wl-org-todo-file "Tasks") "* PROJECT %?\n :PROPERTIES:\n :CREATED: %u\n :END:\n\n %i" :prepend t) ("pw" "Project for work" entry (file+headline ,wl-org-work-file "Inbox") "* PROJECT %?\n :PROPERTIES:\n :CREATED: %u\n :END:\n\n %i" :prepend t) ("c" "Code comment" entry (file+headline ,wl-org-code-file "Code comments") "* %?\n :PROPERTIES:\n :CREATED: %u\n :REFERENCE: [[%(wl-org-capture-original-buffer-line-number)]]\n :END:\n\n#+begin_src %(wl-capture-original-buffer-mode)\n%i\n#+end_src\n\n") ("t" "Creating tasks") ("tt" "Todo" entry (file+headline ,wl-org-todo-file "Tasks") "* TODO %?\n :PROPERTIES:\n :CREATED: %u\n :END:\n\n %i" :prepend t) ("tr" "Reading/Listening/Viewing something" entry (file+headline ,wl-org-todo-file "Tasks") "* TOREAD %?\n :PROPERTIES:\n :CREATED: %u\n :END:\n\n %i" :prepend t) ("ts" "For son" entry (file+headline ,wl-org-son-file "Inbox") "* TODO %?\n :PROPERTIES:\n :CREATED: %u\n :END:\n\n %i" :prepend t) ("to" "Reading/Listening/Viewing for org-protocol" entry (file+headline ,wl-org-todo-file "Tasks") "* TOREAD %:description\n :PROPERTIES:\n :CREATED: %u\n :END:\n\n Source: %:link\n\n %i" :prepend t) ("n" "Note" entry (file+headline ,wl-org-notes-file "Notes") "* %U %?\n\n %i" :prepend t) ("a" "Account" table-line (file+headline "~/edata/liang.org.gpg" "Web accounts") "| %? | | %a | %U |"))) (dolist (todo (list wl-org-gtd-file wl-org-son-file wl-org-work-file wl-org-todo-file wl-org-misc-file)) (when (file-exists-p todo) (add-to-list 'org-agenda-files todo))) (setq org-agenda-text-search-extra-files (file-expand-wildcards (expand-file-name "archive-*.org" org-directory))) (dolist (extra (list wl-org-notes-file wl-org-refs-file)) (when (file-exists-p extra) (add-to-list 'org-agenda-text-search-extra-files extra)))) (setq-default org-tag-persistent-alist '((:startgroup . nil) ("paper") ("video") ("kindle") ("book") ("www") ("podcast") ("magazine") (:endgroup . nil) ("finance") ("gtd" . ?g) ("edu") ("writing") ("coding") ("thinking") ("leading") ("planning" . ?p) (:startgroup . nil) ("NEXT" . ?n) ("someday" . ?s) (:endgroup . nil))) ;;; task switch (defun wl-org-agenda-clock-in-prev-task () (org-agenda-clock-goto) (org-clock-in '(4)) (let ((current-prefix-arg nil)) (org-agenda-redo))) (defun wl-org-clock-in-previous-task () "Clock in previous clocked task." (interactive) (if (and (org-clocking-p) (eq major-mode 'org-agenda-mode)) (wl-org-agenda-clock-in-prev-task) ;; Both `save-excursion' and `save-current-buffer' fail to restore ;; buffer. So use this poor man solution. (let ((cb (current-buffer))) (org-clock-goto) ; Probably by this (call to `switch-to-buffer')? (org-clock-in (if (org-clocking-p) '(4) nil)) (switch-to-buffer cb))) (org-save-all-org-buffers)) (defun wl-org-task-dispatch (&optional arg) (interactive "P") (if (equal arg '(4)) (wl-org-clock-in-previous-task) (org-capture))) (define-key global-map [(f8)] 'wl-org-task-dispatch) ;;; export (unless (fboundp 'org-export-as-latex) ;; exists in old version of org (require 'org-export-latex)) (defun wl-org-export-as-latex () (interactive) (let ((org-export-latex-special-keyword-regexp (concat "^[ \t]*" org-export-latex-special-keyword-regexp))) (org-export-as-latex 3 nil nil nil t nil))) (defun wl-org-export-region-as-html-string (beg end) (interactive "r") (save-excursion (org-export-region-as-html beg end t 'string))) (defun wl-org-export-table-as-html () (interactive) (unless (org-at-table-p) (error "No table at point")) (let* ((beg (org-table-begin)) (end (org-table-end)) (buffer (org-export-region-as-html beg end t "*Org Table HTML Export*"))) (switch-to-buffer-other-window buffer))) (eval-after-load 'yasnippet '(yas/define-snippets 'org-mode '(("beamer" "#+startup: beamer #+LaTeX_CLASS: beamer #+BEAMER_FRAME_LEVEL: 2 #+OPTIONS: ^:nil #+TITLE: $1 #+AUTHOR: Wang Liang #+latex_header: \\usepackage{lmodern} #+latex_header: \\hypersetup{colorlinks=true,linkcolor=blue,urlcolor=blue} #+latex_header: \\mode{\\usetheme{Boadilla}} #+latex_header: \\AtBeginSection[]{\\begin{frame}\\frametitle{Topic}\\tableofcontents[currentsection]\\end{frame}} * $0" "beamer presentation") ("ditaa" "#+begin_src ditaa :file $1 :cmdline ${2:-S -E -r} $0 #+end_src" "ditaa diagram")))) ;;; publish (require 'org-publish) (defvar wl-org-publish-project-root-directory "~/project/wanglianghome.org/orgpublish/") (setq org-publish-project-alist `(("org" :base-directory ,(expand-file-name "src/" wl-org-publish-project-root-directory) :publishing-directory ,(expand-file-name "pub/" wl-org-publish-project-root-directory) :recursive t :section-numbers nil :table-of-contents nil) ("emacsbook" :base-directory ,(expand-file-name "emacsbook/" wl-org-publish-project-root-directory) :base-extension "org" :publishing-directory ,(expand-file-name "pub/programming/emacsbook/" wl-org-publish-project-root-directory)) ("wl-org" :components ("org" "emacsbook")))) ;;; statistics (defun wl-org-entry-get-clocking () (interactive) (remove nil (mapcar (lambda (item) (if (equal "CLOCK" (car item)) (cdr item) nil)) (org-entry-properties)))) (defun wl-org-entry-get-clocking-date () (interactive) (remove "" (mapcar (lambda (item) (if (string-match "\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\).*\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)" item) (let ((beg (match-string 1 item)) (end (match-string 2 item))) (if (string-equal beg end) beg end)) "")) (wl-org-entry-get-clocking)))) (defun wl-org-entry-get-clocking-time (date) (interactive) (remove "" (mapcar (lambda (item) (if (string-match (concat date ".*=> \\([0-9]+:[0-9]+\\)") item) (match-string 1 item) "")) (wl-org-entry-get-clocking)))) (defun wl-split-time-hour-minute (time) (if (string-match "\\([0-9]+\\):\\([0-9]+\\)" time) (cons (match-string 1 time) (match-string 2 time)) nil)) (defun wl-to-minutes (time) (if (stringp time) (let ((l (wl-split-time-hour-minute time))) (+ (string-to-int (car l)) (string-to-int (cdr l)))) time)) (defun wl-org-entry-get-clocking-time-sum (date) (interactive) (reduce (lambda (a b) (+ (wl-to-minutes a) (wl-to-minutes b))) (wl-org-entry-get-clocking-time date))) (defun wl-org-show-current-entry-statistics () (interactive) (let (stat) (dolist (date (wl-org-entry-get-clocking-date) stat) (add-to-list 'stat (cons date (wl-org-entry-get-clocking-time-sum date)))) (when stat (wl-org-show-statistics (list (cons (org-get-heading) stat)))))) (defun wl-org-show-statistics-table (table) (insert "| DATE | CLOCKSUM |\n") (insert "|---\n") (mapcar (lambda (item) (insert (concat "| " (car item) " | " (int-to-string (wl-to-minutes (cdr item))) " |\n"))) table) (insert "|---\n") (forward-line -1) (org-table-align) (forward-line 1)) (defun wl-org-show-statistics (output) (with-current-buffer (get-buffer-create "*Org Statistics*") (erase-buffer) (org-mode) (mapc (lambda (table) (insert (concat "* " (car table) "\n\n")) (wl-org-show-statistics-table (cdr table)) (insert "\n")) output)) (switch-to-buffer-other-window "*Org Statistics*")) (defun wl-org-show-current-subtree-statistics () (interactive) (let (output) (org-map-entries (lambda () (let (stat) (dolist (date (wl-org-entry-get-clocking-date) stat) (when date (add-to-list 'stat (cons date (wl-org-entry-get-clocking-time-sum date))))) (when stat (add-to-list 'output (cons (org-get-heading) stat))))) nil 'tree) (wl-org-show-statistics output))) ;;; sort project task according to close date (defun wl-org-sort-project-by-closed-timestamp () "Sort tasks under current project by CLOSED timestamp. TODO entries are before DONE entries. It makes task operations quick and easy." (interactive) (unless (or (string-equal "PROJECT" (org-get-todo-state)) (string-equal "CLOSE" (org-get-todo-state))) (error "You can only sort project")) (let ((beg (save-excursion (org-back-to-heading t) (point))) (end (save-excursion (org-forward-same-level 1) (point)))) (goto-char beg) (push-mark end) (org-sort-entries nil ?r nil nil "CLOSED"))) (add-hook 'org-babel-after-execute-hook 'org-display-inline-images) (org-babel-do-load-languages 'org-babel-load-languages '((emacs-lisp . t) (ditaa . t))) (defun wl-org-confirm-babel-evaluate (lang body) (not (string= lang "ditaa"))) (setq org-confirm-babel-evaluate 'wl-org-confirm-babel-evaluate) (defun wl-org-show-comments-for-current-file () (interactive) (let* ((current-filename (substring (buffer-file-name) (length (getenv "HOME")))) (org-agenda-files (append (wl-expand-org-file-list '("code.org")) (wl-find-files "~/gtd/code-comment" t ".org$")))) (org-search-view nil current-filename))) (defun wl-org-code-comment-files () (wl-find-files "~/gtd/code-comment" t "\\.org$")) (defun wl-org-capture-original-buffer-line-number (&optional filename) (let ((line-number (number-to-string (with-current-buffer (org-capture-get :original-buffer) (line-number-at-pos)))) (original-buffer-file-name (with-current-buffer (org-capture-get :original-buffer) (buffer-file-name)))) (if filename (concat filename "::" line-number) (concat "~" (substring original-buffer-file-name (length (getenv "HOME"))) "::" line-number)))) (defun wl-capture-original-buffer-mode () (let ((mode-name (symbol-name (with-current-buffer (org-capture-get :original-buffer) major-mode)))) (substring mode-name 0 (- (length mode-name) (length "-mode"))))) (provide 'wl-org)