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.
""))
#+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.
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:
#+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))
(defun eshell-command-to-string (command)
"Return results of executing COMMAND in an eshell environtment.
The COMMAND can either be a string or a list."
(when (listp command)
;; Since `eshell-command' accepts a string (and we want all its
;; other goodies), we synthesize a string, but since `command'
;; 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
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
*** Supporting Functions
*** Getopts
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
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)
(puthash 'parameters (cons arg rest) retmap))
((plist-get defarg :help)
(error (documentation (plist-get defarg :help))))
;; If argument definition has a integer parameter,
;; convert next entry as a number and process rest:
((eq (plist-get defarg :parameter) 'integer)
@ -379,6 +336,70 @@ Lets make some test examples:
(should (stringp (first parms)))
(should (bufferp (second parms))))))
#+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)
Eshell can send the output of a command sequence to a buffer:
#+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
#+begin_src emacs-lisp
(defun eshell/flow (&rest args)
"Output the contents of one or more buffers as a string.
Usage: flow [OPTION] [BUFFER ...]
-h, --help show this usage screen
-l, --lines output contents as a list of lines
-w, --words output contents as a list of space-separated elements "
(let* ((options (eshell-getopts '((:name words :short "w" :long "words")
(:name lines :short "l" :long "lines")
(:name string :short "s" :long "string")
(:name help :short "h" :long "help"))
args))
(buffers (gethash 'parameters options))
(content (thread-last parameters
(-map 'eshell-flow-buffer-contents)
(s-join "\n"))))
(if (gethash 'help options)
(error (documentation 'eshell/flow))
(defun eshell/flow (&rest args)
"Output the contents of one or more buffers as a string.
Usage: flow [OPTION] [BUFFER ...]
-h, --help show this usage screen
-l, --lines output contents as a list of lines
-w, --words output contents as a list of space-separated elements "
(let* ((options (eshell-getopts '((:name words :short "w" :long "words")
(:name lines :short "l" :long "lines")
(:name string :short "s" :long "string")
(:name help :short "h" :long "help"
:help eshell/flow))
args))
(buffers (gethash 'parameters options))
(content (thread-last parameters
(-map 'eshell-flow-buffer-contents)
(s-join "\n"))))
(if (gethash 'help options)
(error (documentation 'eshell/flow))
;; No buffer specified? Use the default buffer's contents:
(unless buffers
(setq content
(eshell-flow-buffer-contents ha-eshell-ebbflow-buffername)))
;; No buffer specified? Use the default buffer's contents:
(unless buffers
(setq content
(eshell-flow-buffer-contents ha-eshell-ebbflow-buffername)))
;; Do we need to convert the output to lines or split on words?
(cond
((gethash 'words options) (split-string content))
((gethash 'lines options) (split-string content "\n"))
(t content)))))
;; Do we need to convert the output to lines or split on words?
(cond
((gethash 'words options) (split-string content))
((gethash 'lines options) (split-string content "\n"))
(t content)))))
#+end_src
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")
(:name append :short "a" :long "append")
(:name prepend :short "p" :long "prepend")
(:name help :short "h" :long "help"))
(:name help :short "h" :long "help"
:help eshell/ebb))
args))
(location (cond
((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.
#+begin_src emacs-lisp
(defun eshell/map (&rest args)
(defun eshell/do (&rest args)
"Execute a command sequence over a collection of file elements.
Separate the sequence and the elements with a `::' string.
For instance:
map chown _ angela :: *.org(u'oscar')
do chown _ angela :: *.org(u'oscar')
The function substitutes the `_' sequence to a single filename
element, and if not specified, it appends the file name to the
command. So the following works as expected:
map chmod a+x :: *.org"
do chmod a+x :: *.org"
(seq-let (forms elements) (-split-on "::" args)
(dolist (element (-flatten (-concat elements)))
;; Replace the _ or append the filename:
(message "Working on %s ... %s" element forms)
(let* ((form (if (-contains? forms "_")
(-replace "_" element forms)
(-snoc forms element)))
@ -926,6 +949,118 @@ b.txt
a.org
8:Nam vestibulum accumsan nisl.
#+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
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