Permalink
| (use-package org :ensure t | |
| :defer t | |
| :commands (gf/find-current-month-notes-file | |
| gf/toggle-switch-to-project-org-file) | |
| :config | |
| (require 'defuns-org) | |
| (setq org-directory "~/notes/") | |
| (setq org-listen-read-watch-file (concat org-directory "topics/listen-read-watch.org")) | |
| ;; Split up the search string on whitespace | |
| (setq org-agenda-search-view-always-boolean t) | |
| (setq org-refile-targets '((nil :maxlevel . 2))) | |
| (defun gf/org-reload () | |
| "Reload the org file for the current month - useful for a long | |
| running emacs instance." | |
| (interactive) | |
| (setq gf/current-month-notes-last-visited nil) | |
| (setq org-files (append (file-expand-wildcards (concat org-directory "*/*.org")) | |
| (file-expand-wildcards (concat org-directory "*/*/*.org")))) | |
| ;; Notes are grouped by month in dates/ for automatic archival. | |
| ;; At the start of every month, move over notes that are still relevant. | |
| ;; Agenda files are only used for searching - this setup is | |
| ;; designed to work without scheduling, tags etc | |
| (setq org-agenda-files (append | |
| (file-expand-wildcards (concat org-directory "dates/*.org")) | |
| (file-expand-wildcards (concat org-directory "topics/*.org")) | |
| (file-expand-wildcards (concat org-directory "topics/*/*.org")))) | |
| (setq org-default-notes-file (gf/org-current-month-notes-file))) | |
| (defun gf/org-current-month-notes-file () | |
| "Get the path of the org file for the current month." | |
| (concat org-directory "dates/" | |
| (downcase (format-time-string "%Y-%B.org")))) | |
| (defun gf/org-refile-files-first () | |
| "Choose an org file to file in, then pick the node. This prevents | |
| emacs opening all of the refile targets at once." | |
| (interactive) | |
| (let ((file (list (completing-read "Refile to: " org-files nil t)))) | |
| (let ((org-refile-targets `((,file :maxlevel . 1)))) | |
| (org-refile))) | |
| (org-save-all-org-buffers)) | |
| (defun gf/commit-notes () | |
| "Commit all org files to git with the current date and time. New files must be explicitly added - this prevents accidental committing of junk files" | |
| (interactive) | |
| (let ((old-dir default-directory)) | |
| (cd org-directory) | |
| (shell-command (concat "git add -u . && git commit -m \"" (format-time-string "%a %e %b %H:%M:%S\""))) | |
| (cd old-dir))) | |
| (defvar gf/current-month-notes-last-visited nil | |
| "The last date the org file for the current month was opened.") | |
| (defun gf/find-current-month-notes-file () | |
| "Find the org file for the current month" | |
| (interactive) | |
| (setq gf/current-month-notes-last-visited (format-time-string "%D")) | |
| (find-file org-default-notes-file)) | |
| (defun gf/check-current-month-notes-reminder () | |
| "Show a reminder message if the current notes file hasn't been visited today." | |
| (if (not (equal gf/current-month-notes-last-visited (format-time-string "%D"))) | |
| (message (format "Check your notes for today, %s" (format-time-string "%A %e of %B"))))) | |
| (add-hook 'find-file-hook 'gf/check-current-month-notes-reminder) | |
| (defun gf/org-end-of-section () | |
| "Move to the last line of the current section." | |
| (interactive) | |
| (re-search-backward "^\* ") | |
| (org-forward-element 1) | |
| (previous-line 1)) | |
| (defun gf/org-beginning-of-line () | |
| "Move to the beginning of the line in an org-mode file, ignoring | |
| TODO keywords, stars and list indicators." | |
| (interactive) | |
| (beginning-of-line) | |
| (if (looking-at-p " ") (evil-forward-word-begin)) | |
| (if (looking-at-p "*") (evil-forward-word-begin)) | |
| (if (looking-at-p "TODO\\|DONE\\|NEXT\\|WAITING\\|CANCELLED") (evil-forward-word-begin))) | |
| (defun gf/org-insert-beginning-of-line () | |
| "Run `gf/org-beginning-of-line` and change to insert state." | |
| (interactive) | |
| (gf/org-beginning-of-line) | |
| (evil-insert-state t)) | |
| (defun gf/org-change-line () | |
| "Change the current line, keeping any org headers and todo titles." | |
| (interactive) | |
| (org-show-subtree) | |
| (end-of-line) | |
| (let ((eol (point))) | |
| (gf/org-beginning-of-line) | |
| (evil-change (point) eol))) | |
| (setq org-todo-keywords | |
| '((sequence "TODO(t)" "NEXT(n)" "|" "DONE(d)") | |
| (sequence "WAITING(w)" "|" "CANCELLED(c)"))) | |
| (setq org-log-done t) | |
| (setq org-todo-keyword-faces | |
| (quote (("TODO" :foreground "#dc322f" :weight bold) | |
| ("DONE" :foreground "forest green" :weight bold :strike-through t) | |
| ("WAITING" :foreground "#89BDFF" :weight bold)))) | |
| (setq org-enforce-todo-dependencies t ; can't close without subtasks being done | |
| org-use-fast-todo-selection t) | |
| (add-hook 'org-capture-mode-hook #'evil-insert-state) | |
| (setq org-capture-templates | |
| '(("t" "Todo" entry (file+headline org-default-notes-file "Tasks") | |
| "* TODO %?" :prepend t) | |
| ("n" "Note" entry (file+headline org-default-notes-file "Notes") | |
| "* %?") | |
| ("x" "Project todo" entry (function gf/org-select-project-file-header) | |
| "* TODO %?") | |
| ("z" "Project note" entry (function gf/org-select-project-file-header) | |
| "* %?") | |
| ("l" "Listen" entry (file+headline org-listen-read-watch-file "Listen") | |
| "* %?") | |
| ("r" "Read" entry (file+headline org-listen-read-watch-file "Read") | |
| "* %?") | |
| ("w" "Watch" entry (file+headline org-listen-read-watch-file "Watch") | |
| "* %?"))) | |
| (defun gf/org-make-capture-frame () | |
| "Make a new frame for using org-capture." | |
| (interactive) | |
| (make-frame '((name . "capture") (width . 80) (height . 20))) | |
| (select-frame-by-name "capture") | |
| (org-capture)) | |
| ;; Behaviour for capturing notes using make-capture-frame | |
| (defadvice org-capture-finalize | |
| (after delete-capture-frame activate) | |
| "Advise capture-finalize to close the frame" | |
| (if (equal "capture" (frame-parameter nil 'name)) | |
| (delete-frame))) | |
| (defadvice org-capture-destroy | |
| (after delete-capture-frame activate) | |
| "Advise capture-destroy to close the frame" | |
| (if (equal "capture" (frame-parameter nil 'name)) | |
| (delete-frame))) | |
| (defadvice org-switch-to-buffer-other-window | |
| (after supress-window-splitting activate) | |
| "Delete the extra window if we're in a capture frame" | |
| (if (equal "capture" (frame-parameter nil 'name)) | |
| (delete-other-windows))) | |
| (org-babel-do-load-languages | |
| 'org-babel-load-languages | |
| '( | |
| (emacs-lisp . t) | |
| (js . t) | |
| (lilypond . t) | |
| (haskell . t) | |
| (python . t) | |
| (sh . t) | |
| )) | |
| (setq org-confirm-babel-evaluate nil) | |
| (setq org-src-fontify-natively t) | |
| (setq org-src-tab-acts-natively t) | |
| (defvar org-projects-dir (expand-file-name "~/notes/projects")) | |
| (defun gf/create-org-path (path) | |
| "Create a name suitable for an org file from the last part of a file | |
| path." | |
| (let ((last (car (last (split-string (if (equal (substring path -1) "/") | |
| (substring path 0 -1) path) "/"))))) | |
| (concat org-projects-dir "/" | |
| (downcase | |
| (replace-regexp-in-string | |
| "\\." "-" (if (equal (substring last 0 1) ".") | |
| (substring last 1) last))) | |
| ".org"))) | |
| (defvar gf/org-project-file-override-alist '() | |
| "An association list of projectile directories and the project org file for them. | |
| This enables overriding the default behaviour of `gf/org-resolve-project-org-file'. | |
| CAR must be an absolute path to a project, including a trailing slash. | |
| CDR must be a path to an org file, relative to `org-directory'. | |
| Example: | |
| \'((\"/home/emacs/some-company/some-project\" \"projects/some-company.org\") | |
| (\"/home/emacs/some-company/different-project\" \"projects/some-company.org\"))") | |
| (defun gf/org-resolve-project-org-file () | |
| "Get the path of the org file for the current project, either by creating a | |
| suitable name automatically or fetching from gf/org-project-file-override-alist." | |
| (if (assoc (projectile-project-root) gf/org-project-file-override-alist) | |
| (concat org-directory (cadr (assoc (projectile-project-root) gf/org-project-file-override-alist))) | |
| (gf/create-org-path (projectile-project-root)))) | |
| (defun gf/org-switch-to-project-org-file () | |
| "Switch to the org file for the current project." | |
| (interactive) | |
| (find-file (gf/org-resolve-project-org-file))) | |
| (defvar gf/previous-project-buffers (make-hash-table :test 'equal)) | |
| (defun gf/toggle-switch-to-project-org-file () | |
| "Alternate between the current buffer and the org file for the | |
| current project." | |
| (interactive) | |
| (if (and | |
| (string-equal "org-mode" (symbol-name major-mode)) | |
| (s-contains-p "/notes/" (buffer-file-name))) | |
| (if (gethash (buffer-file-name) gf/previous-project-buffers) | |
| (switch-to-buffer (gethash (buffer-file-name) gf/previous-project-buffers)) | |
| (error "Previous project buffer not found")) | |
| (let ((file (gf/org-resolve-project-org-file))) | |
| (puthash file (current-buffer) gf/previous-project-buffers) | |
| (find-file file)))) | |
| (defvar gf/org-months '("january" "february" "march" "april" "may" "june" "july" "august" "september" "october" "november" "december")) | |
| (defun gf/org-calculate-month-file-offset (filename offset) | |
| "Calculate the name of the neighbouring month file to FILENAME. | |
| FILENAME is expected to be of the form <year>-<monthname>, e.g. 2016-november. | |
| OFFSET is t for next month, or nil for previous month." | |
| (let* ((pieces (split-string filename "-")) | |
| (year (string-to-number (car pieces))) | |
| (month (cadr pieces)) | |
| (month-number (position month gf/org-months :test 'equal))) | |
| (if offset | |
| (if (eq month-number 11) | |
| (concat (number-to-string (+ year 1)) "-january") | |
| (concat (number-to-string year) "-" (nth (+ month-number 1) gf/org-months))) | |
| (if (eq month-number 0) | |
| (concat (number-to-string (- year 1)) "-december") | |
| (concat (number-to-string year) "-" (nth (- month-number 1) gf/org-months)))))) | |
| (defun gf/org-go-to-next-month () | |
| "Go to the next month org file." | |
| (interactive) | |
| (find-file (concat org-directory "dates/" (gf/org-calculate-month-file-offset (buffer-file-name-body) t) ".org"))) | |
| (defun gf/org-go-to-previous-month () | |
| "Go to the previous month org file." | |
| (interactive) | |
| (find-file (concat org-directory "dates/" (gf/org-calculate-month-file-offset (buffer-file-name-body) nil) ".org"))) | |
| (use-package time-ext :ensure t) | |
| (defun gf/org-previous-month-name-from-filename (filename) | |
| (when (stringp filename) | |
| (let ((index (position (cadr (split-string filename "-")) gf/org-months :test 'equal))) | |
| (capitalize | |
| (if (eq 0 index) "december" (nth (- index 1) gf/org-months)))))) | |
| (defun gf/org-generate-weekly-reviews (filename) | |
| "Generate org task headers for weekly reviews for the given notes file FILENAME. | |
| FILENAME is expected to be of the form <year>-<monthname>, e.g. 2016-november." | |
| (when (stringp filename) | |
| (string-join | |
| (mapcar | |
| (lambda(i) | |
| (concat | |
| "** TODO Review of week beginning " i "\n" | |
| "*** TODO Type up written notes\n" | |
| "*** TODO Import notes from email\n" | |
| "*** TODO Log work, timesheets, goal tracking\n" | |
| "*** TODO Cleanup downloads folder and home directory\n" | |
| "*** TODO Record taxable income and expenses, save 30% of income\n" | |
| "*** TODO Backups" | |
| )) | |
| (mapcar (lambda (date) | |
| (format "%d%s of %s" (nth 3 date) (ordinal-suffix (nth 3 date)) (capitalize (nth (- (nth 4 date) 1) gf/org-months)))) | |
| (gf/org-week-starts filename))) | |
| "\n"))) | |
| (defun gf/org-week-starts (filename) | |
| "Get the mondays from weeks that intersect a given month for notes file FILENAME. | |
| This will also include a monday from the previous month, if the week | |
| intersects with the current month. | |
| Dates are returned in the style from `decode-time'." | |
| (when (stringp filename) | |
| (let* ((pieces (split-string filename "-")) | |
| (year (string-to-number (car pieces))) | |
| (month (cadr pieces)) | |
| (month-number (+ 1 (position month gf/org-months :test 'equal))) | |
| (dates '())) | |
| (progn | |
| ;; get all sundays in the month | |
| (let ((date (decode-time (next-nweek 0 (parse-time-string (format "1:00:00 %d %s %d" 1 month year)))))) | |
| (while (eq month-number (nth 4 date)) | |
| (setq dates (append dates (list date))) | |
| (setq date (decode-time (time-add-day date 7))))) | |
| ;; account for the 1st being a Sunday, and skipped by next-nweek (first will be 8th) | |
| (when (eq 8 (nth 3 (car dates))) | |
| (push (decode-time (time-add-day (car dates) -7)) dates)) | |
| ;; subtract 6 days to get the week beginning for every sunday | |
| (mapcar (lambda (date) (decode-time (time-add-day date -6))) dates))))) | |
| ;; from `diary.el' (`diary-ordinal-suffix') | |
| (defun ordinal-suffix (n) | |
| "Ordinal suffix for N. That is, `st', `nd', `rd', or `th', as appropriate." | |
| (if (or (memq (% n 100) '(11 12 13)) (< 3 (% n 10))) | |
| "th" | |
| (aref ["th" "st" "nd" "rd"] (% n 10)))) | |
| (defun gf/find-org-info-file () | |
| "Open info.org, full of delicious secrets." | |
| (interactive) | |
| (find-file (concat org-directory "topics/info.org"))) | |
| ;; (defun gf/create-project-branch-from-org-heading () | |
| ;; "Create a git feature branch for the current org heading. The project is guessed from the current org file.") | |
| (gf/org-reload) | |
| (general-define-key | |
| :states '(normal visual insert emacs) | |
| :keymaps 'org-mode-map | |
| :prefix gf/major-mode-leader-key | |
| :non-normal-prefix gf/major-mode-non-normal-leader-key | |
| "r" 'gf/org-refile-files-first | |
| "R" 'org-refile | |
| "." 'gf/org-go-to-next-month | |
| "," 'gf/org-go-to-previous-month) | |
| (general-define-key | |
| :states '(normal visual insert emacs) | |
| :prefix "SPC" | |
| :non-normal-prefix "M-SPC" | |
| :keymaps 'org-mode-map | |
| "a" 'gf/org-select-top-level-header-or-all | |
| "A" 'gf/org-select-next-task) | |
| (general-define-key | |
| :states '(normal visual insert) | |
| :keymaps 'org-mode-map | |
| "M-l" 'org-metaright | |
| "M-h" 'org-metaleft | |
| "M-k" 'org-metaup | |
| "M-j" 'org-metadown | |
| "M-L" 'org-shiftmetaright | |
| "M-H" 'org-shiftmetaleft | |
| "M-K" 'org-shiftmetaup | |
| "M-J" 'org-shiftmetadown) | |
| (general-define-key | |
| :states '(normal visual) | |
| :keymaps 'org-mode-map | |
| "^" 'gf/org-beginning-of-line | |
| "I" 'gf/org-insert-beginning-of-line | |
| "S" 'gf/org-change-line | |
| "t" 'org-todo | |
| "TAB" 'org-cycle)) | |
| (provide 'setup-org) |