#+TITLE: Capturing Notes with Org #+AUTHOR: Howard X. Abrams #+EMAIL: howard.abrams@gmail.com #+DATE: 2020-09-18 #+FILETAGS: :emacs: A literate programming file for configuring org for capturing notes. #+BEGIN_SRC emacs-lisp :exports none ;;; capturing-notes.el --- A literate programming file for configuring org for capturing notes. -*- lexical-binding: t; -*- ;; ;; Copyright (C) 2020 Howard X. Abrams ;; ;; Author: Howard X. Abrams ;; Maintainer: Howard X. Abrams ;; Created: September 18, 2020 ;; ;; This file is not part of GNU Emacs. ;; ;; *NB:* Do not edit this file. Instead, edit the original literate file at: ;; ~/other/hamacs/ha-capturing-notes.org ;; And tangle the file to recreate this one. ;; ;;; Code: #+END_SRC * Introduction Capturing (or collecting) notes from files, browsers, and meetings, is a great way to get organized. I even have external commands that kick-off the capturing process, and without a command this is what gets called: #+BEGIN_SRC emacs-lisp (setq org-capture-default-template "c") #+END_SRC Let's now define my templates. * Templates Just make sure we can execute this code anytime, let's just define the variable that will hold all the templates: #+BEGIN_SRC emacs-lisp (defvar org-capture-templates (list)) #+END_SRC Some templates put the information /in front/ of other information (as opposed to the default of appending), so I define a helper function: #+BEGIN_SRC emacs-lisp (defun ha-first-header () (goto-char (point-min)) (search-forward-regexp "^\* ") (beginning-of-line 1) (point)) #+END_SRC ** General Notes Capturing text into the =org-default-notes-file= is something I don't do much: #+BEGIN_SRC emacs-lisp (add-to-list 'org-capture-templates '("n" "Thought or Note" entry (file org-default-notes-file) "* %?\n\n %i\n\n See: %a" :empty-lines 1)) (add-to-list 'org-capture-templates '("w" "Website Announcement" entry (file+function "~/website/index.org" ha-first-header) (file "~/.spacemacs.d/templates/website-announcement.org") :empty-lines 1)) #+END_SRC Before we go too far, we should create a publishing file for the website announcement, and something for the journal. ** Clock in Tasks Org has one task at a time that can be /clocked in/ keeping a timer. I use that as a /destination/ for collecting notes. For instance, capturing with a =c= allows me to just enter stuff in under that task without switching to it: #+BEGIN_SRC emacs-lisp (add-to-list 'org-capture-templates '("c" "Currently clocked in task")) #+END_SRC Let's put a bullet item under that task: #+BEGIN_SRC emacs-lisp (add-to-list 'org-capture-templates `("cc" "Item to Current Clocked Task" item (clock) "%i%?" :empty-lines 1)) #+END_SRC We can select a /region/ and copy that using =c r=: #+BEGIN_SRC emacs-lisp (add-to-list 'org-capture-templates `("cr" "Contents to Current Clocked Task" plain (clock) "%i" :immediate-finish t :empty-lines 1)) #+END_SRC If we have copied anything into the clipboard, that information can be add to the current task using =c k=: #+BEGIN_SRC emacs-lisp (add-to-list 'org-capture-templates `("ck" "Kill-ring to Current Clocked Task" plain (clock) "%c" :immediate-finish t :empty-lines 1)) #+END_SRC Instead, if I am looking at some code, I can copy some code from a region, but use a helper function to create a /link/ to the original source code using =c f=: #+BEGIN_SRC emacs-lisp (add-to-list 'org-capture-templates `("cf" "Code Reference with Comments to Current Task" plain (clock) "%(ha-org-capture-code-snippet \"%F\")\n\n %?" :empty-lines 1)) #+END_SRC If I want a reference to the code, without any comments, I call ~c l~: #+BEGIN_SRC emacs-lisp (add-to-list 'org-capture-templates `("cl" "Link to Code Reference to Current Task" plain (clock) "%(ha-org-capture-code-snippet \"%F\")" :empty-lines 1 :immediate-finish t)) #+END_SRC ** Capture Helper Functions In order to have a capture back-ref to a function and its code, we need to use this: #+BEGIN_SRC emacs-lisp (require 'which-func) #+END_SRC This helper function given a code /type/ and the /function/, analyzes the current buffer in order to collects data about the source code file. It then creates a nice-looking template: #+BEGIN_SRC emacs-lisp (defun ha-org-capture-fileref-snippet (f type headers func-name) (let* ((code-snippet (buffer-substring-no-properties (mark) (- (point) 1))) (file-name (buffer-file-name)) (file-base (file-name-nondirectory file-name)) (line-number (line-number-at-pos (region-beginning))) (initial-txt (if (null func-name) (format "From [[file:%s::%s][%s]]:" file-name line-number file-base) (format "From ~%s~ (in [[file:%s::%s][%s]]):" func-name file-name line-number file-base)))) (format " %s #+BEGIN_%s %s %s #+END_%s" initial-txt type headers code-snippet type))) #+END_SRC For typical code references, we can get the label for Org's =SRC= block by taking the =major-mode= and removing the =-mode= part. We can then call the formatter we previously defined: #+BEGIN_SRC emacs-lisp (defun ha-org-capture-code-snippet (f) "Given a file, F, this captures the currently selected text within an Org SRC block with a language based on the current mode and a backlink to the function and the file." (with-current-buffer (find-buffer-visiting f) (let ((org-src-mode (replace-regexp-in-string "-mode" "" (format "%s" major-mode))) (func-name (which-function))) (ha-org-capture-fileref-snippet f "SRC" org-src-mode func-name)))) #+END_SRC Let's assume that we want to copy some text from a file, but it isn't source code, then this function makes an =EXAMPLE= of it. #+BEGIN_SRC emacs-lisp (defun ha-org-capture-clip-snippet (f) "Given a file, F, this captures the currently selected text within an Org EXAMPLE block and a backlink to the file." (with-current-buffer (find-buffer-visiting f) (ha-org-capture-fileref-snippet f "EXAMPLE" "" nil))) #+END_SRC ** Code Capturing Functions In order to easily call a capture for code, let's make two interactive functions, one just copies the stuff, and the other pulls up a capturing window for comments: #+BEGIN_SRC emacs-lisp (defun ha-code-to-clock (&optional start end) "Send the currently selected code to the currently clocked-in org-mode task." (interactive) (org-capture nil "F")) (defun ha-code-comment-to-clock (&optional start end) "Send the currently selected code (with comments) to the currently clocked-in org-mode task." (interactive) (org-capture nil "f")) #+END_SRC * External Interface ** Emacs Server Control Sure the Emacs application will almost always have the =server-start= going, however, I need to control it just a bit (because I often have two instances running on some of my machines). What /defines/ the Emacs instance for work changes ... often: #+BEGIN_SRC emacs-lisp (defun ha-emacs-for-work? () "Return non-nil when the Emacs application's location matches as one for work. Currently, this is the `emacs-plus' app that I have built with the native-comp model, but I reserve the right to change this." (->> Info-default-directory-list (first) (s-split "/") (--filter (s-starts-with? "emacs-plus" it)) (first))) #+END_SRC #+BEGIN_SRC emacs-lisp (if (ha-emacs-for-work?) (setq server-name "work") (setq server-name "personal")) (server-start) #+END_SRC * External Capturing :LOGBOOK: CLOCK: [2021-05-25 Tue 13:35]--[2021-05-25 Tue 14:05] => 0:30 :END: If we put something on the clipboard using =xclip= or something, and then perhaps =emacsclient= could call this function to put those contents into clocked in task. #+BEGIN_SRC emacs-lisp (defun ha-external-capture-to-org () "Calls `org-capture-string' on the contents of the Apple clipboard." (interactive) (org-capture-string (ha-org-clipboard) "ck") (ignore-errors (delete-frame))) #+END_SRC The =en= script is used as the last pipe entry on the command line, this displays the output, and then copies the contents into the Emacs-based engineering notebook at the currently clocked in task. #+BEGIN_SRC shell :shebang "#!/bin/bash" :tangle ~/bin/en # Interface to my Engineering Notebook. # # Used as the last pipe entry on the command line, this displays the output, # and then copies the contents into the Emacs-based engineering notebook at the # currently clocked in task. # # And parameters to the script are added at the end of a list entry. function usage { echo "$(basename $0) [ -t header-title ] [ -n notes ] [ -f format ] command arguments" exit 1 } while getopts "t:n:f:" o do case "$o" in t) TITLE="$OPTARG";; n) NOTE="$OPTARG";; f) FORMAT="$OPTARG";; [?]) usage;; esac done shift $(expr $OPTIND - 1) COMMAND=$* FILE=$(mktemp) function process_output { cat -v $1 | sed 's/\^\[\[[0-9][0-9]*\(;[0-9][0-9]*\)*m//g' } # The script can either take a command specified as arguments (in # which case, it will run that), or it will assume all data is coming # from standard in... if [ -z "$COMMAND" ] then # All data should be coming from standard in, so capture it: tee $FILE else # Otherwise, we need to run the command: ${COMMAND} | tee $FILE fi # Either way, let's process the results stored in the file: RESULTS=$(process_output $FILE) function output { if [ -n "$TITLE" ] then echo "*** ${TITLE}" fi if [ -n "$NOTE" ] then echo "${NOTE}" fi if [ -n "$COMMAND" ] then echo "#+BEGIN_SRC sh" echo "${COMMAND}" echo "#+END_SRC" echo echo "#+RESULTS:" fi if [ -n "$FORMAT" ] then echo "#+BEGIN_SRC ${FORMAT}" echo "${RESULTS}" echo "#+END_SRC" else echo "#+BEGIN_EXAMPLE" echo "${RESULTS}" echo "#+END_EXAMPLE" fi } if which pbcopy 2>&1 >/dev/null then output | pbcopy else output | xclip fi # Now that the results are on the clipboard, the `c k` capture # sequence calls my "grab from the clipboard" capture template: emacsclient -s work -e '(org-capture-string "" "ck")' rm -f $FILE #+END_SRC * Keybindings Along with kicking off the org-capture, I want to be able to clock-in and out: #+BEGIN_SRC emacs-lisp (general-evil-define-key 'normal org-mode-map :prefix "SPC m" "X" 'org-capture "c" '(:ignore t :which-key "clocks") "c i" '("clock in" . org-clock-in) "c l" '("clock in last" . org-clock-in-last) "c o" '("clock out" . org-clock-out) "c c" '("cancel" . org-clock-cancel) "c d" '("mark default task" . org-clock-mark-default-task) "c e" '("modify effort" . org-clock-modify-effort-estimate) "c E" '("set effort" . org-set-effort) "c g" '("goto clock" . org-clock-goto) "c r" '("resolve clocks" . org-resolve-clocks) "c R" '("clock report" . org-clock-report) "c t" '("eval range" . org-evaluate-time-range) "c =" '("timestamp up" . org-clock-timestamps-up) "c -" '("timestamp down" . org-clock-timestamps-down)) #+END_SRC * Technical Artifacts :noexport: Let's provide a name so we can =require= this file. #+BEGIN_SRC emacs-lisp :exports none (provide 'ha-capturing-notes) ;;; ha-capturing-notes.el ends here #+END_SRC Before you can build this on a new system, make sure that you put the cursor over any of these properties, and hit: ~C-c C-c~ #+DESCRIPTION: A literate programming file for configuring org for capturing notes. #+PROPERTY: header-args:sh :tangle no #+PROPERTY: header-args:emacs-lisp :tangle yes #+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes #+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil #+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil #+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js