Engineering Notebook and Eshell

This is a big feature allows me to easily capture both commands and
output from eshell into my "General Notes". The idea is that I could
edit/massage that content to keep that file small.
This commit is contained in:
Howard Abrams 2022-10-31 20:58:46 -07:00
parent a5179c12d2
commit ac71278fdd

View file

@ -177,71 +177,25 @@ Calling Emacs functions that take a single argument from =eshell= that could acc
;; probably isn't helpful to display in the `eshell' window. ;; probably isn't helpful to display in the `eshell' window.
"")) ""))
#+end_src #+end_src
** Less and More The =eshell-command= is supposed to be an interactive command for prompting for a shell command in the mini-buffer. However, I have some functions that run a command and gather the output. For that, we call =eshell-command= but a =t= for the second argument:
While I can type =find-file=, I often use =e= as an alias for =emacsclient= in Terminals, so lets do something similar for =eshell=:
Also note that we can take advantage of the =eshell-fn-on-files= function to expand the [[help:find-file][find-file]] (which takes one argument), to open more than one file at one time.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defun eshell/e (&rest files) (defun eshell-command-to-string (command)
"Essentially an alias to the `find-file' function." "Return results of executing COMMAND in an eshell environtment.
(eshell-fn-on-files 'find-file 'find-file-other-window files)) The COMMAND can either be a string or a list."
(when (listp command)
(defun eshell/ee (&rest files) ;; Since `eshell-command' accepts a string (and we want all its
"Edit one or more files in another window." ;; other goodies), we synthesize a string, but since `command'
(eshell-fn-on-files 'find-file-other-window 'find-file-other-window files)) ;; could be a parsed list, we quote all of the arguments.
;;
;; Hacky. Until I figure out a better way to call eshell,
;; as `eshell-named-command' doesn't work reliably:
(setq command (s-join " " (cons (first command)
(--map (format "\"%s\"" it) (rest command))))))
(with-temp-buffer
(eshell-command command t)
(buffer-string)))
#+end_src #+end_src
No way would I accidentally type any of the following commands: *** Getopts
#+begin_src emacs-lisp
(defalias 'eshell/emacs 'eshell/e)
(defalias 'eshell/vi 'eshell/e)
(defalias 'eshell/vim 'eshell/e)
#+end_src
Both =less= and =more= are the same to me. as I want to scroll through a file. Sure the [[https://github.com/sharkdp/bat][bat]] program is cool, but from eshell, we could call [[help:view-file][view-file]], and hit ~q~ to quit and return to the shell.
#+begin_src emacs-lisp
(defun eshell/less (&rest files)
"Essentially an alias to the `view-file' function."
(eshell-fn-on-files 'view-file 'view-file-other-window files))
#+end_src
Do I type =more= any more than =less=?
#+begin_src emacs-lisp
(defalias 'eshell/more 'eshell/less)
(defalias 'eshell/view 'eshell/less)
#+end_src
** Ebb and Flow output to Emacs Buffers
This is an interesting experiment.
Typing a command, but the output isnt right. So you punch the up arrow, and re-run the command, but this time pass the output through executables like =tr=, =grep=, and even =awk=. Still not right? Rinse and repeat. Tedious. Since using Emacs to edit text is what we do best, what if we took the output of a command from Eshell, edit that output in a buffer, and then use that edited output in further commands?
I call this workflow of sending command output back and forth into an Emacs buffer, an /ebb/ and /flow/ approach, where the =ebb= function (for Edit a Bumped Buffer … or something like that), takes some command output, and opens it in a buffer (with an =ebbflow= minor mode), allowing us to edit or alter the data. Pull that data back to the Eshell session with the [[help:emacs/flow][flow]] function (for Fetch buffer data by Lines or Words … naming is hard).
*** The ebbflow Buffer
If I dont specify a specific buffer name, we use this default value:
#+begin_src emacs-lisp
(defvar ha-eshell-ebbflow-buffername "*eshell-edit*"
"The name of the buffer that eshell can use to store temporary input/output.")
#+end_src
This buffer has a minor-mode that binds ~C-c C-q~ to close the window and return to the Eshell that spawned it:
#+begin_src emacs-lisp
(defun ha-eshell-ebbflow-return ()
"Close the ebb-flow window and return to Eshell session."
(interactive)
(when (boundp 'ha-eshell-ebbflow-close-window)
(bury-buffer))
(when (boundp 'ha-eshell-ebbflow-return-buffer)
(pop-to-buffer ha-eshell-ebbflow-return-buffer)))
(define-minor-mode ebbflow-mode
"Get your foos in the right places."
:lighter " ebb"
:keymap (let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-q") 'ha-eshell-ebbflow-return)
map))
#+end_src
Since I use Evil, I also add ~Q~ to call this function:
#+begin_src emacs-lisp
(evil-define-key 'normal ebbflow-mode-map (kbd "Q") 'ha-eshell-ebbflow-return)
#+end_src
*** Supporting Functions
I need a function to analyze command line options. Ive tried to use [[help:eshell-eval-using-options][eshell-eval-using-options]], but it lacks the ability to have both dashed parameter arguments /and/ non-parameter arguments. For instance, I want to type: I need a function to analyze command line options. Ive tried to use [[help:eshell-eval-using-options][eshell-eval-using-options]], but it lacks the ability to have both dashed parameter arguments /and/ non-parameter arguments. For instance, I want to type:
#+begin_src sh #+begin_src sh
flow --lines some-buffer another-buffer flow --lines some-buffer another-buffer
@ -314,6 +268,9 @@ This wee beastie takes a list of arguments given to the function, along with a /
((null defarg) ((null defarg)
(puthash 'parameters (cons arg rest) retmap)) (puthash 'parameters (cons arg rest) retmap))
((plist-get defarg :help)
(error (documentation (plist-get defarg :help))))
;; If argument definition has a integer parameter, ;; If argument definition has a integer parameter,
;; convert next entry as a number and process rest: ;; convert next entry as a number and process rest:
((eq (plist-get defarg :parameter) 'integer) ((eq (plist-get defarg :parameter) 'integer)
@ -379,6 +336,70 @@ Lets make some test examples:
(should (stringp (first parms))) (should (stringp (first parms)))
(should (bufferp (second parms)))))) (should (bufferp (second parms))))))
#+end_src #+end_src
** Less and More
While I can type =find-file=, I often use =e= as an alias for =emacsclient= in Terminals, so lets do something similar for =eshell=:
Also note that we can take advantage of the =eshell-fn-on-files= function to expand the [[help:find-file][find-file]] (which takes one argument), to open more than one file at one time.
#+begin_src emacs-lisp
(defun eshell/e (&rest files)
"Essentially an alias to the `find-file' function."
(eshell-fn-on-files 'find-file 'find-file-other-window files))
(defun eshell/ee (&rest files)
"Edit one or more files in another window."
(eshell-fn-on-files 'find-file-other-window 'find-file-other-window files))
#+end_src
No way would I accidentally type any of the following commands:
#+begin_src emacs-lisp
(defalias 'eshell/emacs 'eshell/e)
(defalias 'eshell/vi 'eshell/e)
(defalias 'eshell/vim 'eshell/e)
#+end_src
Both =less= and =more= are the same to me. as I want to scroll through a file. Sure the [[https://github.com/sharkdp/bat][bat]] program is cool, but from eshell, we could call [[help:view-file][view-file]], and hit ~q~ to quit and return to the shell.
#+begin_src emacs-lisp
(defun eshell/less (&rest files)
"Essentially an alias to the `view-file' function."
(eshell-fn-on-files 'view-file 'view-file-other-window files))
#+end_src
Do I type =more= any more than =less=?
#+begin_src emacs-lisp
(defalias 'eshell/more 'eshell/less)
(defalias 'eshell/view 'eshell/less)
#+end_src
** Ebb and Flow output to Emacs Buffers
This is an interesting experiment.
Typing a command, but the output isnt right. So you punch the up arrow, and re-run the command, but this time pass the output through executables like =tr=, =grep=, and even =awk=. Still not right? Rinse and repeat. Tedious. Since using Emacs to edit text is what we do best, what if we took the output of a command from Eshell, edit that output in a buffer, and then use that edited output in further commands?
I call this workflow of sending command output back and forth into an Emacs buffer, an /ebb/ and /flow/ approach, where the =ebb= function (for Edit a Bumped Buffer … or something like that), takes some command output, and opens it in a buffer (with an =ebbflow= minor mode), allowing us to edit or alter the data. Pull that data back to the Eshell session with the [[help:emacs/flow][flow]] function (for Fetch buffer data by Lines or Words … naming is hard).
*** The ebbflow Buffer
If I dont specify a specific buffer name, we use this default value:
#+begin_src emacs-lisp
(defvar ha-eshell-ebbflow-buffername "*eshell-edit*"
"The name of the buffer that eshell can use to store temporary input/output.")
#+end_src
This buffer has a minor-mode that binds ~C-c C-q~ to close the window and return to the Eshell that spawned it:
#+begin_src emacs-lisp
(defun ha-eshell-ebbflow-return ()
"Close the ebb-flow window and return to Eshell session."
(interactive)
(when (boundp 'ha-eshell-ebbflow-close-window)
(bury-buffer))
(when (boundp 'ha-eshell-ebbflow-return-buffer)
(pop-to-buffer ha-eshell-ebbflow-return-buffer)))
(define-minor-mode ebbflow-mode
"Get your foos in the right places."
:lighter " ebb"
:keymap (let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-q") 'ha-eshell-ebbflow-return)
map))
#+end_src
Since I use Evil, I also add ~Q~ to call this function:
#+begin_src emacs-lisp
(evil-define-key 'normal ebbflow-mode-map (kbd "Q") 'ha-eshell-ebbflow-return)
#+end_src
*** flow (or Buffer Cat) *** flow (or Buffer Cat)
Eshell can send the output of a command sequence to a buffer: Eshell can send the output of a command sequence to a buffer:
#+begin_src sh #+begin_src sh
@ -392,34 +413,35 @@ Im calling the ability to get a buffer contents, /flow/ (Fetch contents as Li
- /as a string/ :: no conversion - /as a string/ :: no conversion
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defun eshell/flow (&rest args) (defun eshell/flow (&rest args)
"Output the contents of one or more buffers as a string. "Output the contents of one or more buffers as a string.
Usage: flow [OPTION] [BUFFER ...] Usage: flow [OPTION] [BUFFER ...]
-h, --help show this usage screen -h, --help show this usage screen
-l, --lines output contents as a list of lines -l, --lines output contents as a list of lines
-w, --words output contents as a list of space-separated elements " -w, --words output contents as a list of space-separated elements "
(let* ((options (eshell-getopts '((:name words :short "w" :long "words") (let* ((options (eshell-getopts '((:name words :short "w" :long "words")
(:name lines :short "l" :long "lines") (:name lines :short "l" :long "lines")
(:name string :short "s" :long "string") (:name string :short "s" :long "string")
(:name help :short "h" :long "help")) (:name help :short "h" :long "help"
args)) :help eshell/flow))
(buffers (gethash 'parameters options)) args))
(content (thread-last parameters (buffers (gethash 'parameters options))
(-map 'eshell-flow-buffer-contents) (content (thread-last parameters
(s-join "\n")))) (-map 'eshell-flow-buffer-contents)
(if (gethash 'help options) (s-join "\n"))))
(error (documentation 'eshell/flow)) (if (gethash 'help options)
(error (documentation 'eshell/flow))
;; No buffer specified? Use the default buffer's contents: ;; No buffer specified? Use the default buffer's contents:
(unless buffers (unless buffers
(setq content (setq content
(eshell-flow-buffer-contents ha-eshell-ebbflow-buffername))) (eshell-flow-buffer-contents ha-eshell-ebbflow-buffername)))
;; Do we need to convert the output to lines or split on words? ;; Do we need to convert the output to lines or split on words?
(cond (cond
((gethash 'words options) (split-string content)) ((gethash 'words options) (split-string content))
((gethash 'lines options) (split-string content "\n")) ((gethash 'lines options) (split-string content "\n"))
(t content))))) (t content)))))
#+end_src #+end_src
Straight-forward to acquire the contents of a buffer : Straight-forward to acquire the contents of a buffer :
@ -469,7 +491,8 @@ We have three separate use-cases:
(let* ((options (eshell-getopts '((:name insert :short "i" :long "insert") (let* ((options (eshell-getopts '((:name insert :short "i" :long "insert")
(:name append :short "a" :long "append") (:name append :short "a" :long "append")
(:name prepend :short "p" :long "prepend") (:name prepend :short "p" :long "prepend")
(:name help :short "h" :long "help")) (:name help :short "h" :long "help"
:help eshell/ebb))
args)) args))
(location (cond (location (cond
((gethash 'insert options) :insert) ((gethash 'insert options) :insert)
@ -704,21 +727,21 @@ Pretty ugly, but what about using =::= as a separator of the /lambda/ from the /
Here is my initial function. After separating the arguments into two groups (split on the =::= string), we iterate over the file elements, creating a /form/ that includes the filename. Here is my initial function. After separating the arguments into two groups (split on the =::= string), we iterate over the file elements, creating a /form/ that includes the filename.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defun eshell/map (&rest args) (defun eshell/do (&rest args)
"Execute a command sequence over a collection of file elements. "Execute a command sequence over a collection of file elements.
Separate the sequence and the elements with a `::' string. Separate the sequence and the elements with a `::' string.
For instance: For instance:
map chown _ angela :: *.org(u'oscar') do chown _ angela :: *.org(u'oscar')
The function substitutes the `_' sequence to a single filename The function substitutes the `_' sequence to a single filename
element, and if not specified, it appends the file name to the element, and if not specified, it appends the file name to the
command. So the following works as expected: command. So the following works as expected:
map chmod a+x :: *.org" do chmod a+x :: *.org"
(seq-let (forms elements) (-split-on "::" args) (seq-let (forms elements) (-split-on "::" args)
(dolist (element (-flatten (-concat elements))) (dolist (element (-flatten (-concat elements)))
;; Replace the _ or append the filename: (message "Working on %s ... %s" element forms)
(let* ((form (if (-contains? forms "_") (let* ((form (if (-contains? forms "_")
(-replace "_" element forms) (-replace "_" element forms)
(-snoc forms element))) (-snoc forms element)))
@ -926,6 +949,118 @@ b.txt
a.org a.org
8:Nam vestibulum accumsan nisl. 8:Nam vestibulum accumsan nisl.
#+end_example #+end_example
** Engineering Notebook
I want both the command and the output (as well as comments) to be able to go into an org-mode file, I call my /engineering notebook/. Where in that file? If I use =en= that goes in a “General Notes” section, and =ec= goes into the currently clocked in task in that file.
I use =ex= to refer to both =en= / =ec=. Use cases:
- =ex <command>= :: run the command given and send the output to the notebook
- =ex [-n #]= :: grab the output from a previously executed command (defaults to last one)
- =ex -c "<comment>" <command>= :: run command and write the comment to the current date in the notebook
- =ex <command> :: <comment>= :: run command and write comment to the notebook
- =<command> > ex= :: write output from /command/ to the notebook. This wont add the command that generated the output.
The =-c= option can be combined with the /command/, but I dont want it to grab the last output, as I think I would just like to send text to the notebook as after thoughts. If the option to =-c= is blank, perhaps it just calls the capture template to allow me to enter voluminous content.
This requires capture templates that dont do any formatting. I will reused =c c= from [[file:ha-capturing-notes.org::*General Notes][capturing-notes]] code, and create other templates under =e= prefix:
#+begin_src emacs-lisp
;; (setq org-capture-templates nil)
(add-to-list 'org-capture-templates
'("e" "Engineering Notebook"))
(add-to-list 'org-capture-templates
'("ee" "Notes and Commentary" plain
(file+olp+datetree org-default-notes-file "General Notes")
"%i" :empty-lines 1 :tree-type month :unnarrowed t))
(add-to-list 'org-capture-templates
'("ef" "Piped-in Contents" plain
(file+olp+datetree org-default-notes-file "General Notes")
"%i" :immediate-finish t :empty-lines 1 :tree-type month))
#+end_src
#+begin_src emacs-lisp
(defun ha-eshell-engineering-notebook (capture-template args)
"Capture commands and output from Eshell into an Engineering Notebook.
Usage: ex [ options ] [ command string ] [ :: prefixed comments ]]
A _command string_ is an eshell-compatible shell comman to run,
and if not given, uses previous commands in the Eshell history.
Options:
-c, --comment A comment string displayed before the command
-n, --history The historical command to use, where `0' is the
previous command, and `1' is the command before that.
-t, --template The `keys' string to specify the capture template"
(let* (output
(options (eshell-getopts
'((:name comment :short "c" :long "comment" :parameter string)
(:name history :short "n" :long "history" :parameter integer)
(:name captemp :short "t" :long "template" :parameter string)
(:name interact :short "i" :long "interactive")
(:name help :short "h" :long "help"
:help ha-eshell-engineering-notebook))
args))
(sh-call (gethash 'parameters options))
(sh-parts (-split-on "::" sh-call))
(command (s-join " " (first sh-parts)))
;; Combine the -c parameter with text following ::
(comment (s-join " " (cons (gethash 'comment options)
(second sh-parts))))
(history (or (gethash 'history options) 0)))
;; Given a -t option? Override the default:
(when (gethash 'captemp options)
(setq capture-template (gethash 'captemp options)))
(when (gethash 'interact options)
(setq capture-template "ee"))
(cond
(sh-call ; Gotta a command, run it!
(ha-eshell-engineering-capture capture-template comment command
(eshell-command-to-string (first sh-parts))))
(t ; Otherwise, get the history
(ha-eshell-engineering-capture capture-template comment
(ring-ref eshell-history-ring (1+ history))
(eshell/output history))))))
(defun ha-eshell-engineering-capture (capture-template comment cmd out)
"Capture formatted string in CAPTURE-TEMPLATE.
Base the string created on COMMENT, CMD, and OUT. Return OUTPUT."
(let* ((command (s-trim cmd))
(output (s-trim out))
(results (concat
(when comment (format "%s\n\n" comment))
(when command (format "#+begin_src shell\n %s\n#+end_src\n\n" command))
(when (and command output) "#+results:\n")
(when output (format "#+begin_example\n%s\n#+end_example\n" output)))))
(message results)
(org-capture-string results capture-template)
;; Return output from the command, or nothing if there wasn't anything:
(or output "")))
#+end_src
And now we have a =en= and a =ec= version:
#+begin_src emacs-lisp
(defun eshell/en (&rest args)
"Call `ha-eshell-engineering-notebook' to \"General Notes\"."
(interactive)
(ha-eshell-engineering-notebook "ef" args))
(defun eshell/ec (&rest args)
"Call `ha-eshell-engineering-notebook' to current clocked-in task."
(interactive)
(ha-eshell-engineering-notebook "cc" args))
#+end_src
This function simply calls [[help-org-capture][org-capture]] with [[info:org#Template elements][a template]]:
#+begin_src emacs-lisp
(defun eshell/cap (&rest args)
"Call `org-capture' with the `ee' template to enter text into the engineering notebook."
(org-capture nil "ee"))
#+end_src
* Special Prompt * Special Prompt
Following [[http://blog.liangzan.net/blog/2012/12/12/customizing-your-emacs-eshell-prompt/][these instructions]], we build a better prompt with the Git branch in it (Of course, it matches my Bash prompt). First, we need a function that returns a string with the Git branch in it, e.g. ":master" Following [[http://blog.liangzan.net/blog/2012/12/12/customizing-your-emacs-eshell-prompt/][these instructions]], we build a better prompt with the Git branch in it (Of course, it matches my Bash prompt). First, we need a function that returns a string with the Git branch in it, e.g. ":master"
#+begin_src emacs-lisp :tangle no #+begin_src emacs-lisp :tangle no