diff --git a/README.org b/README.org index d0eb563..1edef0c 100644 --- a/README.org +++ b/README.org @@ -21,7 +21,8 @@ This creates [[file:~/.emacs.d/init.el][~/.emacs.d/init.el]] that starts the pro - [[file:ha-org-clipboard.org][org-clipboard]] :: automatically converting HTML from a clipboard into Org-formatted content. - [[file:ha-org-journaling.org][org-journaling]] :: for writing journal entries and tasks. - [[file:ha-org-publishing.org][org-publishing]] :: code for publishing my website, [[http://howardism.org][www.howardism.org]]. - - [[file:ha-org-sprint.org][org-sprint]] :: functions for working with the my Org-focused sprint file + - [[file:ha-org-sprint.org][org-sprint]] :: functions for working with the my Org-focused sprint file. + - [[file:ha-eshell.org][eshell]] :: customization and enhancement to the Emacs shell. - [[file:ha-remoting.org][remoting]] :: my interface to systems using SSH and Vterm. - [[file:ha-email.org][email]] :: reading email using =notmuch= in a *Hey* fashion. - [[file:ha-feed-reader.org][feed-reader]] :: configuration of elfeed as well as my RSS feeds. diff --git a/bootstrap.org b/bootstrap.org index 2534c94..f34a45d 100644 --- a/bootstrap.org +++ b/bootstrap.org @@ -193,28 +193,29 @@ The following loads the rest of my org-mode literate files. I add new filesas t (defvar ha-hamacs-files (flatten-list `("ha-private.org" "ha-config.org" - ,(when (display-graphic-p) - "ha-display.org") + ,(when (display-graphic-p) + "ha-display.org") "ha-org.org" - ,(when (display-graphic-p) - "ha-org-word-processor.org") - "ha-org-clipboard.org" - "ha-capturing-notes.org" - "ha-agendas.org" - "ha-passwords.org" - "ha-remoting.org" - "ha-programming.org" - "ha-programming-elisp.org" - "ha-programming-python.org" - ,(if (ha-emacs-for-work?) - '("ha-org-sprint.org" "ha-work.org") - ;; Personal Editor - '("ha-org-journaling.org" - "ha-irc.org" - "ha-org-publishing.org" - "ha-email.org" - "ha-aux-apps.org" - "ha-feed-reader.org")))) + ,(when (display-graphic-p) + "ha-org-word-processor.org") + "ha-org-clipboard.org" + "ha-capturing-notes.org" + "ha-agendas.org" + "ha-passwords.org" + "ha-eshell.org" + "ha-remoting.org" + "ha-programming.org" + "ha-programming-elisp.org" + "ha-programming-python.org" + ,(if (ha-emacs-for-work?) + '("ha-org-sprint.org" "ha-work.org") + ;; Personal Editor + '("ha-org-journaling.org" + "ha-irc.org" + "ha-org-publishing.org" + "ha-email.org" + "ha-aux-apps.org" + "ha-feed-reader.org")))) "List of org files that complete the hamacs project.") #+end_src diff --git a/ha-eshell.org b/ha-eshell.org new file mode 100644 index 0000000..8e7bb58 --- /dev/null +++ b/ha-eshell.org @@ -0,0 +1,582 @@ +#+TITLE: Eshell +#+AUTHOR: Howard X. Abrams +#+DATE: 2022-09-13 +#+FILETAGS: :emacs: + +A literate programming file for configuring the Emacs Shell. +#+begin_src emacs-lisp :exports none + ;;; ha-eshell --- Emacs Shell configuration. -*- lexical-binding: t; -*- + ;; + ;; © 2022 Howard X. Abrams + ;; This work is licensed under a Creative Commons Attribution 4.0 International License. + ;; See http://creativecommons.org/licenses/by/4.0/ + ;; + ;; Author: Howard X. Abrams + ;; Maintainer: Howard X. Abrams + ;; Created: September 13, 2022 + ;; + ;; While obvious, GNU Emacs does not include this file or project. + ;; + ;; *NB:* Do not edit this file. Instead, edit the original literate file at: + ;; /Users/howard.abrams/other/hamacs/ha-eshell.org + ;; And tangle the file to recreate this one. + ;; + ;;; Code: + #+end_src +* Introduction +While I like [[https://github.com/akermu/emacs-libvterm][vterm]], especially for logging into [[file:ha-remoting.org][remote systems]], I find Emacs’ shell, =eshell=, an interesting alternative. +If you find the documentation lacking, I [[http://www.howardism.org/Technical/Emacs/eshell-fun.html][documented most features]], and you might find the following helpful. +** Navigation and Keys +Along with the regular Emacs keybindings, Eshell comes with some interesting features: + - ~M-RET~ can be used to accumulate further commands while a command is currently running. Since all input is passed to the subprocess being executed, there is no automatic input queueing as there is with other shells. + - ~C-c C-t~ can be used to truncate the buffer if it grows too large. + - ~C-c C-r~ will move point to the beginning of the output of the last command. With a prefix argument, it will narrow to view only that output. + - ~C-c C-o~ will delete the output from the last command. + - ~C-c C-f~ will move forward a complete shell argument. + - ~C-c C-b~ will move backward a complete shell argument. +** Control-D Double Duty +Used to ~C-d~ exiting from a shell? Want it to keep working, but still allow deleting a character? We can have it both (thanks to [[https://github.com/wasamasa/dotemacs/blob/master/init.org#eshell][wasamasa]]): +#+begin_src emacs-lisp + (defun ha-eshell-quit-or-delete-char (arg) + "The `C-d' sequence closes window or deletes a character." + (interactive "p") + (if (and (eolp) (looking-back eshell-prompt-regexp)) + (progn + (eshell-life-is-too-much) ; Why not? (eshell/exit) + (ignore-errors + (delete-window))) + (delete-forward-char arg))) + #+END_SRC +** Pager Setup +If any program wants to pause the output through the =$PAGER= variable, well, we don't really need that: +#+begin_src emacs-lisp + (setenv "PAGER" "cat") +#+end_src +* Aliases +Gotta have some [[http://www.emacswiki.org/emacs/EshellAlias][shell aliases]], right? We have three ways of doing that. First, enter them into an =eshell= session: +#+begin_src sh + alias less 'view-file $1' +#+end_src +Note that you need single quotes, and multiple arguments don’t really work with aliases. + +Second, you can create/populate the alias file, =~/.emacs.d/eshell/alias= … as long as you don’t use those single quotes: +#+begin_src shell :tangle ~/.emacs.d/eshell/alias + alias ll ls -l $* + alias clear recenter 0 + alias d dired $1 + alias e find-file $1 + alias less view-file $1 + alias more view-file $1 + alias find echo 'Please use fd instead.' +#+end_src +Which happens when you type those commands into an =eshell=. + +Third, you want /control/, write a function to define the aliases: +#+begin_src emacs-lisp :tangle no + (defun ha-eshell-add-aliases () + "Call `eshell/alias' to define my aliases." + (eshell/alias "e" "find-file $1") + (eshell/alias "d" "dired $1") + (eshell/alias "gd" "magit-diff-unstaged") + (eshell/alias "gds" "magit-diff-staged") + + ;; The 'ls' executable requires the Gnu version on the Mac + (let ((ls (if (file-exists-p "/usr/local/bin/gls") + "/usr/local/bin/gls" + "/bin/ls"))) + (eshell/alias "ll" (concat ls " -AlohG --color=always")))) +#+end_src +* Predicate Filters and Modifiers +The =T= predicate filter allows me to limit file results that have have internal =org-mode= tags. For instance, files that have a =#+TAGS:= header with a =mac= label will be given to the =grep= function: +#+begin_src sh + $ grep brew *.org(T'mac') +#+end_src + +As described in [[http://www.howardism.org/Technical/Emacs/eshell-fun.html][this essay]], to extend Eshell, we need a two-part function: + 1. Parse the Eshell buffer to look for the parameter (and move the point past the parameter). + 2. A predicate function that takes a file as a parameter. + +For the first step, we have our function /called/ as it helps parse the text at this time. Based on what it sees, it returns the predicate function used to filter the files: +#+begin_src emacs-lisp + (defun eshell-org-file-tags () + "Helps the eshell parse the text the point is currently on, + looking for parameters surrounded in single quotes. Returns a + function that takes a FILE and returns nil if the file given to + it doesn't contain the org-mode #+TAGS: entry specified." + + ;; Step 1. Parse the eshell buffer for our tag between quotes + ;; Make sure to move point to the end of the match: + (if (looking-at (rx "'" (group (one-or-more (not (or ")" "'"))))"'")) + (let* ((tag (match-string 1)) + (reg (rx line-start + "#+" (optional "file") "tags:" + (one-or-more space) + (zero-or-more any) + (literal tag) word-end))) + (goto-char (match-end 0)) + + ;; Step 2. Return the predicate function: + ;; Careful when accessing the `reg' variable. + `(lambda (file) + (with-temp-buffer + (insert-file-contents file) + (re-search-forward ,reg nil t 1)))) + (error "The `T' predicate takes an org-mode tag value in single quotes."))) +#+END_src +Then we need add that function to the =eshell-predicate-alist= as the =T= tag: +#+begin_src emacs-lisp + (defun ha-eshell-add-predicates () + "A hook to add a `eshell-org-file-tags' predicate filter to eshell." + (add-to-list 'eshell-predicate-alist '(?T . (eshell-org-file-tags)))) +#+end_src +*Note:* We can’t add it to the list until after we start our first eshell session, so we just add it to the =eshell-pred-load-hook= which is sufficient. +* Eshell Functions +Any function that begins with =eshell/= can be called with the remaining letters. I used to have a function =eshell/f= as a replacement for =find=, but the [[https://github.com/sharkdp/fd][fd]] project is better. I used to have a number =g=-prefixed aliases to call git-related commands, but now, I just need to call [[file:ha-config.org::*Magit][Magit]] instead. + +However, since =eshell= is an /Emacs/ shell, I want to think of how to use Emacs buffers in a shell-focused workflow. For instance, use =view-file= instead of =less=, as it will show a file with syntax coloring, and typing ~q~ returns to your shell session. +** Git +My =gst= command is just an alias to =magit-status=, but using the =alias= doesn't pull in the current working directory, so I make it a function, instead: +#+begin_src emacs-lisp + (defun eshell/gst (&rest args) + (magit-status (pop args) nil) + (eshell/echo)) ;; The echo command suppresses output +#+end_src +** Editing Files +Which an alias to [[help:find-file][find-file]] (which takes one argument), we could define a special function that can take multiple arguments, and open them in different windows. We first define a /helper function/ for dealing with multiple arguments. It takes two functions, the first function is called on the first argument, and the second function is called on each of the rest. +#+begin_src emacs-lisp + (defun eshell-fn-on-files (fun1 fun2 args) + (unless (null args) + (let ((filenames (thread-last args + (reverse) + (-flatten) + (-map 'file-expand-wildcards) + (-flatten)))) + (apply fun1 (car filenames)) + (when (cdr filenames) + (-map fun2 (cdr filenames)))))) +#+end_src +This allows us to replace some of our aliases with functions: +#+begin_src emacs-lisp + (defun eshell/e (&rest args) + "Edit one or more files in current window." + (eshell-fn-on-files 'find-file 'find-file-other-window args)) + + (defun eshell/ee (&rest args) + "Edit one or more files in another window." + (eshell-fn-on-files 'find-file-other-window 'find-file-other-window args)) +#+end_src +We’ll leave the =e= alias to replace the =eshell= buffer window. +* 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 + (defun curr-dir-git-branch-string (pwd) + "Returns current git branch as a string, or the empty string if + PWD is not in a git repo (or the git command is not found)." + (interactive) + (when (and (not (file-remote-p pwd)) + (eshell-search-path "git") + (locate-dominating-file pwd ".git")) + (let* ((git-url (shell-command-to-string "git config --get remote.origin.url")) + (git-repo (file-name-base (s-trim git-url))) + (git-output (shell-command-to-string (concat "git rev-parse --abbrev-ref HEAD"))) + (git-branch (s-trim git-output)) + (git-icon "\xe0a0") + (git-icon2 (propertize "\xf020" 'face `(:family "octicons")))) + (concat git-repo " " git-icon2 " " git-branch)))) +#+end_src + +The function takes the current directory passed in via =pwd= and replaces the =$HOME= part with a tilde. I'm sure this function already exists in the eshell source, but I didn't find it... +#+begin_src emacs-lisp :tangle no + (defun pwd-replace-home (pwd) + "Replace home in PWD with tilde (~) character." + (interactive) + (let* ((home (expand-file-name (getenv "HOME"))) + (home-len (length home))) + (if (and + (>= (length pwd) home-len) + (equal home (substring pwd 0 home-len))) + (concat "~" (substring pwd home-len)) + pwd))) +#+end_src + +Make the directory name be shorter...by replacing all directory names with just its first names. However, we leave the last two to be the full names. Why yes, I did steal this. +#+begin_src emacs-lisp :tangle no + (defun pwd-shorten-dirs (pwd) + "Shorten all directory names in PWD except the last two." + (let ((p-lst (split-string pwd "/"))) + (if (> (length p-lst) 2) + (concat + (mapconcat (lambda (elm) (if (zerop (length elm)) "" + (substring elm 0 1))) + (butlast p-lst 2) + "/") + "/" + (mapconcat (lambda (elm) elm) + (last p-lst 2) + "/")) + pwd))) ;; Otherwise, we just return the PWD +#+end_src + +Break up the directory into a "parent" and a "base": +#+begin_src emacs-lisp :tangle no + (defun split-directory-prompt (directory) + (if (string-match-p ".*/.*" directory) + (list (file-name-directory directory) (file-name-base directory)) + (list "" directory))) +#+END_SRC + +Using virtual environments for certain languages is helpful to know, especially since I change them based on the directory. +#+begin_src emacs-lisp :tangle no + (defun ruby-prompt () + "Returns a string (may be empty) based on the current Ruby Virtual Environment." + (let* ((executable "~/.rvm/bin/rvm-prompt") + (command (concat executable "v g"))) + (when (file-exists-p executable) + (let* ((results (shell-command-to-string executable)) + (cleaned (string-trim results)) + (gem (propertize "\xe92b" 'face `(:family "alltheicons")))) + (when (and cleaned (not (equal cleaned ""))) + (s-replace "ruby-" gem cleaned)))))) + + (defun python-prompt () + "Returns a string (may be empty) based on the current Python + Virtual Environment. Assuming the M-x command: `pyenv-mode-set' + has been called." + (when (fboundp #'pyenv-mode-version) + (let ((venv (pyenv-mode-version))) + (when venv + (concat + (propertize "\xe928" 'face `(:family "alltheicons")) + (pyenv-mode-version)))))) +#+end_src + +Now tie it all together with a prompt function can color each of the prompts components. +#+begin_src emacs-lisp :tangle no + (defun eshell/eshell-local-prompt-function () + "A prompt for eshell that works locally (in that is assumes + that it could run certain commands) in order to make a prettier, + more-helpful local prompt." + (interactive) + (let* ((pwd (eshell/pwd)) + (directory (split-directory-prompt + (pwd-shorten-dirs + (pwd-replace-home pwd)))) + (parent (car directory)) + (name (cadr directory)) + (branch (curr-dir-git-branch-string pwd)) + (ruby (when (not (file-remote-p pwd)) (ruby-prompt))) + (python (when (not (file-remote-p pwd)) (python-prompt))) + + (dark-env (eq 'dark (frame-parameter nil 'background-mode))) + (for-bars `(:weight bold)) + (for-parent (if dark-env `(:foreground "dark orange") `(:foreground "blue"))) + (for-dir (if dark-env `(:foreground "orange" :weight bold) + `(:foreground "blue" :weight bold))) + (for-git `(:foreground "green")) + (for-ruby `(:foreground "red")) + (for-python `(:foreground "#5555FF"))) + + (concat + (propertize "⟣─ " 'face for-bars) + (propertize parent 'face for-parent) + (propertize name 'face for-dir) + (when branch + (concat (propertize " ── " 'face for-bars) + (propertize branch 'face for-git))) + ;; (when ruby + ;; (concat (propertize " ── " 'face for-bars) + ;; (propertize ruby 'face for-ruby))) + ;; (when python + ;; (concat (propertize " ── " 'face for-bars) + ;; (propertize python 'face for-python))) + (propertize "\n" 'face for-bars) + (propertize (if (= (user-uid) 0) " #" " $") 'face `(:weight ultra-bold)) + ;; (propertize " └→" 'face (if (= (user-uid) 0) `(:weight ultra-bold :foreground "red") `(:weight ultra-bold))) + (propertize " " 'face `(:weight bold))))) + + (setq-default eshell-prompt-function #'eshell/eshell-local-prompt-function) +#+end_src +Here is the result: +[[http://imgur.com/nkpwII0.png]] +** Fringe Status +Some prompts, shells and terminal programs that display the exit code as an icon in the fringe. So can the [[http://projects.ryuslash.org/eshell-fringe-status/][eshell-fringe-status]] project. Seems to me, that if would be useful to rejuggle those fringe markers so that the marker matched the command entered (instead of seeing a red mark, and needing to scroll back in order to wonder what command it was that made it). Still... +#+begin_src emacs-lisp + (use-package eshell-fringe-status + :hook (eshell-mode . eshell-fringe-status-mode)) +#+end_src +** Opening Banner +Whenever I open a shell, I instinctively type =ls= … so why not do that automatically? Perhaps we hook into the [[elisp:(describe-variable 'eshell-banner-load-hook)][eshell-banner-load-hook]]: +#+begin_src emacs-lisp :tangle no + (defun ha-eshell-banner (&rest ignored) + "My personal banner." + (insert "ls") + (eshell-send-input)) + + (add-hook 'eshell-banner-load-hook 'ha-eshell-banner) + + (setq eshell-banner-message "") +#+end_src +The only thing I would like is to not have the =ls= shown at the top of the buffer, nor added to the /history/. I’ll work on that some day. +* Shell Windows +Now that I often need to quickly pop into remote systems to run a shell or commands, I create helper functions to create those buffer windows. Each begin with =eshell-=: +** Shell There +The basis for opening an shell depends on the /location/. After that, we make the window smaller, give the buffer a good name, as well as immediately display the files with =ls= (since I instinctively just /do that/ … every time). +#+begin_src emacs-lisp + (defun eshell-there (parent) + "Open an eshell session in a PARENT directory + in a smaller window named after this directory." + (let* ((name (thread-first parent + (split-string "/" t) + (last) + (car))) + (eshell-buffer-name (format "*eshell: %s*" name)) + (height (/ (window-total-height) 3)) + (default-directory parent)) + (split-window-vertically (- height)) + (eshell))) +#+end_src +** Shell Here +This version of the =eshell= is based on the current buffer’s parent directory: +#+begin_src emacs-lisp + (defun eshell-here () + "Opens up a new shell in the directory of the current buffer. + The eshell is renamed to match that directory to make multiple + eshell windows easier." + (interactive) + (eshell-there (if (buffer-file-name) + (file-name-directory (buffer-file-name)) + default-directory))) +#+end_src +And let’s bind it: +#+begin_src emacs-lisp + (bind-key "C-!" 'eshell-here) +#+end_src +** Shell for a Project +I usually want the =eshell= to start in the project’s root, using [[help:projectile-project-root][projectile-project-root]]: +#+begin_src emacs-lisp + (defun eshell-project () + "Open a new shell in the project root directory, in a smaller window." + (interactive) + (eshell-there (projectile-project-root))) +#+end_src +And we can attach this function to the =projectile= menu: +#+begin_src emacs-lisp + (ha-leader "p s" '("shell" . eshell-project)) +#+end_src + +** Shell Over There +Would be nice to be able to run an eshell session and use Tramp to connect to the remote host in one fell swoop: +#+begin_src emacs-lisp + (defun eshell-remote (host) + "Creates an eshell session that uses Tramp to automatically + connect to a remote system, HOST. The hostname can be either the + IP address, or FQDN, and can specify the user account, as in + root@blah.com. HOST can also be a complete Tramp reference." + (interactive "sHost: ") + + (let ((destination-path + (cond + ((string-match-p "^/" host) host) + + ((string-match-p (ha-eshell-host-regexp 'full) host) + (string-match (ha-eshell-host-regexp 'full) host) ;; Why!? + (let* ((user1 (match-string 2 host)) + (host1 (match-string 3 host)) + (user2 (match-string 6 host)) + (host2 (match-string 7 host))) + (if host1 + (ha-eshell-host->tramp user1 host1) + (ha-eshell-host->tramp user2 host2)))) + + (t (format "/%s:" host))))) + (eshell-there destination-path))) + #+END_SRC +** Shell Here to There +Since I have Org files that contains tables of system to remotely connect to, I figured I should have a little function that can jump to a host found listed anywhere on the line. + +The regular expression associated with IP addresses, hostnames, user accounts (of the form, =jenkins@my.build.server=, or even full Tramp references, is a bit...uhm, hairy. And since I want to reuse these, I will hide them in a function: +#+begin_src emacs-lisp + (defun ha-eshell-host-regexp (regexp) + "Returns a particular regular expression based on symbol, REGEXP" + (let* ((user-regexp "\\(\\([[:alpha:].]+\\)@\\)?") + (tramp-regexp "\\b/ssh:[:graph:]+") + (ip-char "[[:digit:]]") + (ip-plus-period (concat ip-char "+" "\\.")) + (ip-regexp (concat "\\(\\(" ip-plus-period "\\)\\{3\\}" ip-char "+\\)")) + (host-char "[[:alpha:][:digit:]-]") + (host-plus-period (concat host-char "+" "\\.")) + (host-regexp (concat "\\(\\(" host-plus-period "\\)+" host-char "+\\)")) + (horrific-regexp (concat "\\b" + user-regexp ip-regexp + "\\|" + user-regexp host-regexp + "\\b"))) + (cond + ((eq regexp 'tramp) tramp-regexp) + ((eq regexp 'host) host-regexp) + ((eq regexp 'full) horrific-regexp)))) +#+END_SRC + +The function to scan a line for hostname patterns uses different function calls that what I could use for =eshell-there=, so let's =save-excursion= and hunt around: +#+begin_src emacs-lisp + (defun ha-eshell-scan-for-hostnames () + "Helper function to scan the current line for any hostnames, IP + or Tramp references. This returns a tuple of the username (if + found) and the hostname. + + If a Tramp reference is found, the username part of the tuple + will be `nil'." + (save-excursion + (goto-char (line-beginning-position)) + (if (search-forward-regexp (ha-eshell-host-regexp 'tramp) (line-end-position) t) + (cons nil (buffer-substring-no-properties (match-beginning 0) (match-end 0))) + + ;; Returns the text associated with match expression, NUM or `nil' if no match was found. + (cl-flet ((ha-eshell-get-expression (num) (if-let ((first (match-beginning num)) + (end (match-end num))) + (buffer-substring-no-properties first end)))) + + (search-forward-regexp (ha-eshell-host-regexp 'full) (line-end-position)) + + ;; Until this is completely robust, let's keep this debugging code here: + ;; (message (mapconcat (lambda (tup) (if-let ((s (car tup)) + ;; (e (cadr tup))) + ;; (buffer-substring-no-properties s e) + ;; "null")) + ;; (-partition 2 (match-data t)) " -- ")) + + (let ((user1 (ha-eshell-get-expression 2)) + (host1 (ha-eshell-get-expression 3)) + (user2 (ha-eshell-get-expression 6)) + (host2 (ha-eshell-get-expression 7))) + (if host1 + (cons user1 host1) + (cons user2 host2))))))) +#+end_src + +Tramp reference can be long when attempting to connect as another user account using the pipe symbol. +#+begin_src emacs-lisp + (defun ha-eshell-host->tramp (username hostname &optional prefer-root) + "Returns a TRAMP reference based on a USERNAME and HOSTNAME + that refers to any host or IP address." + (cond ((string-match-p "^/" host) + host) + ((or (and prefer-root (not username)) (equal username "root")) + (format "/ssh:%s|sudo:%s:" hostname hostname)) + ((or (null username) (equal username user-login-name)) + (format "/ssh:%s:" hostname)) + (t + (format "/ssh:%s|sudo:%s|sudo@%s:%s:" hostname hostname username hostname)))) +#+end_src + +Finally, a function to pull it all together. +#+begin_src emacs-lisp + (defun eshell-here-on-line (p) + "Search the current line for an IP address or hostname, and call the `eshell-here' function. + + Call with PREFIX to connect with the `root' useraccount, via + `sudo'." + (interactive "p") + (destructuring-bind (user host) (ha-eshell-scan-for-hostnames) + (let ((destination (ha-eshell-host->tramp user host (> p 1)))) + (message "Connecting to: %s" destination) + (eshell-there destination)))) +#+end_src + +* Better Command Line History +On [[http://www.reddit.com/r/emacs/comments/1zkj2d/advanced_usage_of_eshell/][this discussion]] a little gem for using IDO to search back through the history, instead of =M-R= to prompt for the history. +#+begin_src emacs-lisp + (defun eshell-insert-history () + "Displays the eshell history to select and insert back into your eshell." + (interactive) + (insert (completing-read "Eshell history: " + (delete-dups + (ring-elements eshell-history-ring))))) + #+END_SRC +* Command on the File Buffer +Sometimes you just need to change something about the current file you are editing...like the permissions or even execute it. Hitting =Command-1= will prompt for a shell command string and then append the current file to it and execute it. +#+begin_src emacs-lisp + (defun execute-command-on-file-buffer (cmd) + "Executes a shell command, CMD, on the current buffer's file. + If the filename is not specified, then it is appended to the cmd, so + + chmod a+x + + Works as expected. The special variable `$$' is replaced with the + filename of the buffer. Note that this is command is passed to + `eshell-command', so eshell modifiers are available, for + instance: + + mv $$ $$(:r).txt + + Will rename the current file to now have a .txt extension. + See `eshell-display-modifier-help' for details on that." + (interactive "sCommand to execute: ") + (let* ((file-name (buffer-file-name)) + (full-cmd (cond ((string-match (rx "$$") cmd) + (replace-regexp-in-string (rx "$$") file-name cmd)) + ((string-match (rx (literal file-name)) cmd) + cmd) + (t + (concat cmd " " file-name))))) + (message "Executing: %s" full-cmd) + (eshell-command full-cmd))) +#+end_src +* Configuration +Here is where we associate all the functions and their hooks with =eshell=, through the magic of =use-package=. + +Scrolling through the output and searching for results that can be copied to the kill ring is a great feature of Eshell. However, instead of running =end-of-buffer= key-binding, the following setting means any other key will jump back to the prompt: +#+begin_src emacs-lisp + (use-package eshell + :straight (:type built-in) + :init + (setq eshell-scroll-to-bottom-on-input 'all + eshell-error-if-no-glob t + eshell-hist-ignoredups t + eshell-save-history-on-exit t + + ;; Since eshell starts fast, let's get rid of it quickly: + eshell-kill-on-exit t + eshell-destroy-buffer-when-process-dies t + + ;; Can you remember the parameter differences between the + ;; executables `chmod' and `find' and their Emacs equivalent? Me + ;; neither, so this makes it act a bit more shell-like: + eshell-prefer-lisp-functions nil) + + :hook ((eshell-pred-load . ha-eshell-add-predicates) + (eshell-exit . delete-window)) + + :bind (:map eshell-mode-map + ("M-R" . eshell-insert-history) + ("C-d" . ha-eshell-quit-or-delete-char))) +#+end_src +Note that the default list to [[emacs-lisp:(describe-variable 'eshell-visual-commands)][eshell-visual-commands]] is mostly good enough. + +Finally, add some leader commands to call my previously defined functions: +#+begin_src emacs-lisp + (ha-leader + "a e" '(:ignore t :which-key "eshell") + "a e e" '("new eshell" . eshell-here) + "a e r" '("remote" . eshell-remote) + "a e p" '("project" . eshell-project) + "a e g" '("at point" . eshell-here-on-line) + "a e !" '("exec on file-buffer" . execute-command-on-file-buffer)) +#+end_src +No, i’m not sure why =use-package= has an issue with both =:hook=, =:bind= and =:config= directives in sequence. +* Technical Artifacts :noexport: +Let's =provide= a name so we can =require= this file: +#+begin_src emacs-lisp :exports none + (provide 'ha-eshell) + ;;; ha-eshell.el ends here + #+end_src + +#+DESCRIPTION: Emacs configuration for the Emacs Shell. + +#+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-remoting.org b/ha-remoting.org index c939aed..1718199 100644 --- a/ha-remoting.org +++ b/ha-remoting.org @@ -23,103 +23,6 @@ A literate configuration for accessing remote systems. ;; ;;; Code: #+end_src -* Local Terminals -The following section configures my Terminal experience, both inside and outside Emacs. -** Eshell -I used to use [[http://www.howardism.org/Technical/Emacs/eshell.html][Eshell all the time]], but now I've migrated most of /work/ directly into Emacs (rewriting all those shell scripts a Emacs Lisp code). A shell is pretty good for my brain at organizing files (old habits, maybe). - -First, my /aliases/ follow me around, and the following creates the alias file, =~/.emacs.d/eshell/alias=: -#+begin_src shell :tangle elisp:(identity eshell-aliases-file) :mkdirp yes - alias ll ls -l $* - alias clear recenter 0 - alias emacs 'find-file $1' - alias e 'find-file $1' - alias ee 'find-file-other-window $1' - alias grep 'rg -n -H --no-heading -e "$*"' - alias find 'echo Please use fd instead.' # :-) -#+end_src - -Since that file already exists (probably), the following command may not be necessary: -#+begin_src emacs-lisp - (use-package eshell - :custom - (eshell-kill-on-exit t) - (eshell-destroy-buffer-when-process-dies t) - - :hook - (eshell-exit . (lambda () (delete-window)))) -#+end_src - -I usually want a new window running Eshell, that is smaller than the current buffer: - -#+begin_src emacs-lisp - (defun eshell-there (parent) - "Open an eshell session in a PARENT directory - in a smaller window named after this directory." - (let ((name (thread-first parent - (split-string "/" t) - (last) - (car))) - (height (/ (window-total-height) 3)) - (default-directory parent)) - (split-window-vertically (- height)) - (eshell "new") - (rename-buffer (concat "*eshell: " name "*")) - - (insert (concat "ls")) - (eshell-send-input))) -#+end_src - -We either want to start the shell in the same parent as the current buffer, or at the root of the project: -#+begin_src emacs-lisp - (defun eshell-here () - "Opens up a new shell in the directory associated with the - current buffer's file. Rename the eshell buffer name to match - that directory to make multiple eshell windows easier." - (interactive) - (eshell-there (if (buffer-file-name) - (file-name-directory (buffer-file-name)) - default-directory))) - - (defun eshell-project () - "Open a new shell in the project root directory, in a smaller window." - (interactive) - (eshell-there (projectile-project-root))) -#+end_src - -Add my org-specific predicates, see this [[http://www.howardism.org/Technical/Emacs/eshell-fun.html][this essay]] for the details: -#+begin_src emacs-lisp - (defun eshell-org-file-tags () - "Helps the eshell parse the text the point is currently on, - looking for parameters surrounded in single quotes. Returns a - function that takes a FILE and returns nil if the file given to - it doesn't contain the org-mode #+FILETAGS: entry specified." - - ;; Step 1. Parse the eshell buffer for our tag between quotes - ;; Make sure to move point to the end of the match: - (if (looking-at "'\\([^)']+\\)'") - (let* ((tag (match-string 1)) - (reg (rx bol "#+FILETAGS: " - (zero-or-more any) - word-start - (literal tag) - word-end - (zero-or-more any) - eol))) - (goto-char (match-end 0)) - - ;; Step 2. Return the predicate function: - ;; Careful when accessing the `reg' variable. - `(lambda (file) - (with-temp-buffer - (insert-file-contents file) - (re-search-forward ,reg nil t 1)))) - (error "The `T' predicate takes an org-mode tag value in single quotes."))) - - (defvar eshell-predicate-alist nil - "A list of predicates than can be applied to a globbing pattern.") - (add-to-list 'eshell-predicate-alist '(?T . (eshell-org-file-tags))) -#+end_src * Remote Editing with Tramp [[https://www.emacswiki.org/emacs/TrampMode][Tramp]] allows almost all Emacs features to execute on a remote system. #+begin_src emacs-lisp @@ -317,14 +220,12 @@ Simply calling =vterm= fails to load my full environment, so this allows me to s #+end_src Before we leave this section, I realize that I would like a way to /add/ to my list of hosts: - #+begin_src emacs-lisp (defun ha-ssh-add-favorite-host (hostname ip-address) "Add a favorite host to your list for easy pickin's." (interactive "sHostname: \nsIP Address: ") (add-to-list 'ha-ssh-favorite-hostnames (cons hostname ip-address))) #+end_src - ** Programmatic Interface The previous functions (as well as my own end of sprint demonstrations) often need to issue some commands to a running terminal session, which is a simple wrapper around a /send text/ and /send return/ sequence: @@ -502,8 +403,6 @@ This file, so far, as been good-enough for a Vanilla Emacs installation, but to #+begin_src emacs-lisp (ha-leader - "a e" '("eshell" . eshell-here) - "a E" '("top eshell" . eshell-project) "a s" '(:ignore t :which-key "ssh") "a s v" '("vterm" . vterm)