From dee00af73b32552eb23d64d0415204371780292e Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Tue, 9 Nov 2021 08:19:16 -0800 Subject: [PATCH] Capturing notes with org Added/converted my `capturing-notes` essay and code, plus a couple of minor org-related bugs. --- README.org | 1 + bootstrap.org | 2 +- ha-capturing-notes.org | 361 +++++++++++++++++++++++++++++++++++++++++ ha-org.org | 24 ++- 4 files changed, 379 insertions(+), 9 deletions(-) create mode 100644 ha-capturing-notes.org diff --git a/README.org b/README.org index c71aa1a..260b42d 100644 --- a/README.org +++ b/README.org @@ -20,5 +20,6 @@ This creates [[file:~/.emacs.d/init.el][~/.emacs.d/init.el]] that starts the pro - [[file:ha-org-clipboard.org][ha-org-clipboard.org]] :: automatically converting HTML from a clipboard into Org-formatted content. - [[file:ha-org-sprint.org][ha-org-sprint.org]] :: functions for working with the my Org-focused sprint files. - [[file:ha-programming.org][ha-programming.org]] :: configuration for /all/ programming languages, or at least, the simple ones. + - [[file:ha-capturing-notes.org][ha-capturing-notes.org]] :: my engineering notebook. *Note:* Other functions and files come from essays written on [[http://www.howardism.org][my blog]]. To help with this, see [[file:support/final-initialize.el][support/final-initialize.el]] file. diff --git a/bootstrap.org b/bootstrap.org index 388e9d2..663cf21 100644 --- a/bootstrap.org +++ b/bootstrap.org @@ -115,7 +115,7 @@ The following loads the rest of my org-mode literate files. I add them as they a ;; "org-journaling.org" ;; "org-publishing.org" "ha-org-sprint.org" - ;; "capturing-notes.org" + "ha-capturing-notes.org" "ha-programming.org" ;; "ha-agendas.org" ;; "ha-email.org" diff --git a/ha-capturing-notes.org b/ha-capturing-notes.org new file mode 100644 index 0000000..d1ffb00 --- /dev/null +++ b/ha-capturing-notes.org @@ -0,0 +1,361 @@ +#+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: +;; /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 diff --git a/ha-org.org b/ha-org.org index d9f125b..30ceca6 100644 --- a/ha-org.org +++ b/ha-org.org @@ -290,12 +290,18 @@ To make the snippets more context aware, this predicate (plist-get (cadr (org-element-at-point)) :language))) #+END_SRC ** Keybindings -Keybindings specific to org files: -#+BEGIN_SRC emacs-lisp :tangle no +Bindings specific to org files: +#+BEGIN_SRC emacs-lisp (general-evil-define-key 'normal org-mode-map :prefix "SPC m" - "e" 'org-export-dispatch - "y" 'org-insert-link) + "e" '("exports" . org-export-dispatch) + "y" '("insert link" . org-insert-link) + + "n" '(:ignore t :which-key "narrow") + "n s" '("subtree" . org-narrow-to-subtree) + "n b" '("block" . org-narrow-to-block) + "n e" '("element" . org-narrow-to-element) + "n w" '("widen" . widen)) #+END_SRC * Supporting Packages At this point, we assume that the =use-package= for org is complete, so we can close it and allow other projects to be loaded: @@ -304,11 +310,13 @@ At this point, we assume that the =use-package= for org is complete, so we can c #+END_SRC ** Exporters Need a few extra exporters: -#+BEGIN_SRC emacs-lisp :tangle no -(use-package ox-md) +#+BEGIN_SRC emacs-lisp + (use-package ox-md + :straight nil) -(use-package ox-confluence - :load-path "~/.doom.d/elisp") + (use-package ox-confluence + :straight nil + :load-path "~/.doom.d/elisp") #+END_SRC And Graphviz configuration using [[https://github.com/ppareit/graphviz-dot-mode][graphviz-dot-mode]]: