362 lines
12 KiB
Org Mode
362 lines
12 KiB
Org Mode
|
#+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 <http://gitlab.com/howardabrams>
|
||
|
;; Maintainer: Howard X. Abrams <howard.abrams@gmail.com>
|
||
|
;; 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:
|
||
|
;; /home/howard/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
|