Created a "lsd" command to call lsd if installed

But format it like the built-in "eshell/ls" command.
This commit is contained in:
Howard Abrams 2022-10-01 21:06:49 -07:00
parent b399c30bb6
commit 62a9262f43

View file

@ -55,27 +55,32 @@ If any program wants to pause the output through the =$PAGER= variable, well, we
* Aliases * 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: 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 #+begin_src sh
alias less 'view-file $1' alias ll 'ls -AlohG --color=always'
#+end_src #+end_src
Note that you need single quotes, and more than one argument doesnt work with aliases. To resolve that, we need to write [[Eshell Functions][a function]]. Note that you need single quotes, and more than one argument doesnt work with aliases. To resolve that, we need to write [[Eshell Functions][a function]].
Second, you can create/populate the alias file, =~/.emacs.d/eshell/alias= … as long as you dont use those single quotes: Second, you can create/populate the alias file, =~/.emacs.d/eshell/alias= … as long as you dont use those single quotes: ~/.emacs.d/eshell/alias
#+begin_src shell :tangle ~/.emacs.d/eshell/alias #+begin_src shell :tangle no
alias ll ls -l $* alias ll ls -AlohG --color=always
alias clear recenter 0 alias d dired
alias d dired $1 alias e find-file
alias e find-file $1
alias less view-file $1 alias less view-file $1
alias more view-file $1 alias more view-file $1
alias find echo 'Please use fd instead.' alias find echo 'Please use fd instead.'
#+end_src #+end_src
Which happens when you type those commands into an =eshell=. Which happens when you type those commands into an =eshell=.
Note: The issues with =alias= include dealing with arguments and calling Emacs Lisp functions, for I would like to have:
#+begin_src sh
alias less view-file
#+end_src
Third, you want /control/, write a function to define the aliases: Third, you want /control/, write a function to define the aliases:
#+begin_src emacs-lisp :tangle no #+begin_src emacs-lisp :tangle no
(defun ha-eshell-add-aliases () (defun ha-eshell-add-aliases ()
"Call `eshell/alias' to define my aliases." "Call `eshell/alias' to define my aliases."
(eshell/alias "e" "find-file $1") (eshell/alias "e" "find-file $1")
(eshell/alias "less" "view-file $1")
(eshell/alias "d" "dired $1") (eshell/alias "d" "dired $1")
(eshell/alias "gd" "magit-diff-unstaged") (eshell/alias "gd" "magit-diff-unstaged")
(eshell/alias "gds" "magit-diff-staged") (eshell/alias "gds" "magit-diff-staged")
@ -134,6 +139,142 @@ Then we need add that function to the =eshell-predicate-alist= as the =T= tag:
Any function that begins with =eshell/= is available as a command (with the remaining letters) Once I had a function =eshell/f= as a replacement for =find=, but the [[https://github.com/sharkdp/fd][fd]] project is better. Any function that begins with =eshell/= is available as a command (with the remaining letters) Once I had a function =eshell/f= as a replacement for =find=, but the [[https://github.com/sharkdp/fd][fd]] project is better.
Since =eshell= is an /Emacs/ shell, I try to think 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. Since =eshell= is an /Emacs/ shell, I try to think 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.
This helper function can tell me if an executable program is available and where it is:
#+begin_src emacs-lisp
(defun ha-find-executable (program)
"Return full path to executable PROGRAM on the `exec-path'."
(first
(-filter 'file-executable-p
(--map (expand-file-name program it) (exec-path)))))
#+end_src
** Foobar
#+begin_src emacs-lisp
(defun eshell/foobar (&rest args)
"The `foobar' in Lisp.
This does little more than print out information given to it as
an example of how to write eshell functions."
(setq args (eshell-flatten-and-stringify args))
(if eshell-in-pipeline-p
(eshell-parse-command (eshell-quote-argument ext-cat) args)
(eshell-eval-using-options
"foobar" args
'((?h "help" nil nil "show this usage screen")
(?l "line" nil single-line "display in a single line")
:show-usage
:usage "[OPTION] TEXT...
Display text, or standard input, to standard output.")
(if single-line
(format "Args: %s" args)
(mapconcat (lambda (word) (format "Arg: %s\n" word)) args "\n")))))
#+end_src
** Replace ls
I like the output of the [[https://github.com/Peltoche/lsd][lsd]] program, and want =ls= to call it, if available.
#+begin_src emacs-lisp
(defvar ha-lsd (ha-find-executable "lsd")
"Location of the `lsd' program, if installed.")
#+end_src
The problem I have with =lsd= is that it does not display in columns or /colorize/ its output in eshell (even when changing the =TERM= variable). Since I already wrote this code, Im re-purposing it and expanding it. Step one is to have a function that gives a list of files for a =directory= (notice it doesnt take options, for if I am going for special output, Ill be calling =ls= directly).
#+begin_src emacs-lisp
(defun ha-eshell-ls-files (&optional directory)
"Return a list of directories in DIRECTORY or `default-directory' if null."
(let ((default-directory (or directory default-directory)))
(if ha-lsd
(shell-command-to-list (format "%s --icon always" ha-lsd))
(directory-files default-directory nil
(rx string-start
(not (any "." "#"))
(one-or-more any)
(not "~")
string-end)))))
#+end_src
Given a filename, lets pad and colorize it based on file attributes:
#+begin_src emacs-lisp
(defun ha-eshell-ls-filename (filename padded-fmt &optional directory)
"Return a prettized version of FILE based on its attributes.
Formats the string with PADDED-FMT."
(let ((file (expand-file-name (if (string-match (rx (group alpha (zero-or-more any))) filename)
(match-string 1 filename)
filename)
directory))
(import-rx (rx "README"))
(image-rx (rx "." (or "png" "jpg" "jpeg" "tif" "wav") string-end))
(code-rx (rx "." (or "el" "py" "rb") string-end))
(docs-rx (rx "." (or "org" "md") string-end)))
(format padded-fmt
(cond
((file-directory-p file)
(propertize filename 'face 'eshell-ls-directory))
((file-executable-p file)
(propertize filename 'face 'eshell-ls-executable))
((string-match import-rx file)
(propertize filename 'face '(:foreground "orange")))
((string-match image-rx file)
(propertize filename 'face 'eshell-ls-special))
((file-symlink-p file)
(propertize filename 'face 'eshell-ls-symlink))
((not (file-readable-p file))
(propertize filename 'face 'eshell-ls-unreadable))
(t
filename)))))
#+end_src
This function pulls all the calls to [[help:ha-eshell-ls-file][ha-eshell-ls-file]] to create columns to make a multi-line string:
#+begin_src emacs-lisp
(defun ha-eshell-ls (&optional directory)
"Return a formatted string of files for a directory.
The string is a pretty version with columns and whatnot."
(let* ((files (ha-eshell-ls-files (or directory default-directory)))
(longest (--reduce-from (max acc (length it)) 1 files))
(width (window-total-width))
(columns (/ width (+ longest 3)))
(padded (if ha-lsd
(format "%%-%ds " longest)
(format "• %%-%ds " longest))))
(cl-flet* ((process-lines (files)
(s-join "" (--map (ha-eshell-ls-filename it padded directory) files)))
(process-files (table)
(s-join "\n" (--map (process-lines it) table))))
(concat (process-files (seq-partition files columns)) "\n\n"))))
#+end_src
While the =ha-eshell-ls= takes a directory, this version puts the canonical directory as a label before the listing. This will be called when we directly specify the directory name(s):
#+begin_src emacs-lisp
(defun ha-eshell-ls-directory (directory)
"Print the DIRECTORY name and its contents."
(let ((dir (file-truename directory)))
(concat
(propertize dir 'face '(:foreground "gold" :underline t))
":\n"
(ha-eshell-ls dir))))
#+end_src
I have the interface program to work with =eshell=.
#+begin_src emacs-lisp
(defun eshell/lsd (&rest args)
(let ((lsd (ha-find-executable "lsd")))
(cond
;; I expect to call this function without any arguments most of the time:
((and lsd (null args))
(ha-eshell-ls))
;; Called with other directories? Print them all, one at a time:
((and lsd (--none? (string-match (rx string-start "-") it) args))
(mapconcat 'ha-eshell-ls-directory args ""))
;; Calling the function with -l or other arguments, don't bother. Just call ls:
(t (eshell/ls args)))))
#+end_src
Which needs an =ls= alias:
#+begin_src emacs-lisp :tangle no
;; (eshell/alias "lss" "echo $@")
#+end_src
** Buffer Cat ** Buffer Cat
Why not be able to read a buffer and use it as the start of a pipeline? Why not be able to read a buffer and use it as the start of a pipeline?
#+begin_src emacs-lisp #+begin_src emacs-lisp
@ -563,49 +704,16 @@ The [[http://projects.ryuslash.org/eshell-fringe-status/][eshell-fringe-status]]
:hook (eshell-mode . eshell-fringe-status-mode)) :hook (eshell-mode . eshell-fringe-status-mode))
#+end_src #+end_src
** Opening Banner ** Opening Banner
Whenever I open a shell, I instinctively type =ls= … so why not do that automatically? The [[elisp:(describe-variable 'eshell-banner-message)][eshell-banner-message]] variable, while normally a string, can be a /form/ (an s-expression) that calls a function, so I made a customized =ls= that can be attractive: Whenever I open a shell, I instinctively type =ls= … so why not do that automatically? The [[elisp:(describe-variable 'eshell-banner-message)][eshell-banner-message]] variable, while defaults to a string, this variable can be a /form/ (an s-expression) that calls a function, so I made a customized =ls= that can be attractive:
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defun ha-eshell-banner () (defun ha-eshell-banner ()
"Return a string containing the files in the current directory." "Return a string containing the files in the current directory."
(let* ((non-hidden (rx string-start (eshell/lsd))
(not (any "." "#"))
(one-or-more any)
(not "~")
string-end))
(files (directory-files default-directory nil non-hidden))
(longest (--reduce-from (max acc (length it)) 1 files))
(padded (format "%%-%ds " longest))
(width (window-total-width))
(columns (/ width (+ longest 3))))
(cl-flet* ((process-file
(file)
(let ((impor-rx (rx string-start "README"))
(image-rx (rx "." (or "png" "jpg" "jpeg" "tif" "wav") string-end))
(code-rx (rx "." (or "el" "py" "rb") string-end))
(docs-rx (rx "." (or "org" "md") string-end)))
(format padded (cond
((string-match impor-rx file)
(propertize file 'face '(:foreground "gold")))
((string-match image-rx file)
(propertize file 'face '(:foreground "light pink")))
((string-match code-rx file)
(propertize file 'face '(:foreground "DarkSeaGreen1")))
((file-directory-p file)
(propertize file 'face 'eshell-ls-directory))
(t
file)))))
(process-lines (files) (s-join "• " (--map (process-file it) files)))
(process-files (table) (s-join "\n" (--map (process-lines it) table))))
(concat (process-files (seq-partition files columns)) "\n\n"))))
#+end_src #+end_src
* Shell Windows * 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-=: Now that I often need to pop into remote systems to run a shell or commands, I create helper functions to create those buffer windows. Each buffer begins with =eshell=: allowing me to have more than one eshells, typically, one per project.
** Shell There ** 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). The basis for distinguishing a shell is its /parent location/. Before starting =eshell=, we make a small window, set the buffer name (using the [[elisp:(describe-variable 'eshell-buffer-name)][eshell-buffer-name]]):
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defun eshell-there (parent) (defun eshell-there (parent)
"Open an eshell session in a PARENT directory. "Open an eshell session in a PARENT directory.
@ -621,12 +729,12 @@ The basis for opening an shell depends on the /location/. After that, we make th
(eshell))) (eshell)))
#+end_src #+end_src
** Shell Here ** Shell Here
This version of the =eshell= is based on the current buffers parent directory: This version of the =eshell= bases the location on the current buffers parent directory:
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defun eshell-here () (defun eshell-here ()
"Opens up a new shell in the directory of the current buffer. "Opens a new shell in the directory of the current buffer.
The eshell is renamed to match that directory to make multiple Renames the eshell buffer to match that directory to allow more
eshell windows easier." than one eshell window."
(interactive) (interactive)
(eshell-there (if (buffer-file-name) (eshell-there (if (buffer-file-name)
(file-name-directory (buffer-file-name)) (file-name-directory (buffer-file-name))
@ -637,7 +745,7 @@ And lets bind it:
(bind-key "C-!" 'eshell-here) (bind-key "C-!" 'eshell-here)
#+end_src #+end_src
** Shell for a Project ** Shell for a Project
I usually want the =eshell= to start in the projects root, using [[help:projectile-project-root][projectile-project-root]]: This version starts =eshell= in the projects root, using [[help:projectile-project-root][projectile-project-root]]:
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defun eshell-project () (defun eshell-project ()
"Open a new shell in the project root directory, in a smaller window." "Open a new shell in the project root directory, in a smaller window."
@ -646,7 +754,7 @@ I usually want the =eshell= to start in the projects root, using [[help:proje
#+end_src #+end_src
And we can attach this function to the =projectile= menu: And we can attach this function to the =projectile= menu:
#+begin_src emacs-lisp #+begin_src emacs-lisp
(ha-leader "p s" '("shell" . eshell-project)) (ha-leader "p t" '("eshell" . eshell-project))
#+end_src #+end_src
** Shell Over There ** Shell Over There
@ -660,20 +768,24 @@ Would be nice to be able to run an eshell session and use Tramp to connect to th
(interactive "sHost: ") (interactive "sHost: ")
(let ((destination-path (let ((destination-path
(cond (cond
((string-match-p "^/" host) host) ;; Is the HOST already an absolute tramp reference?
((string-match-p (rx line-start "/") host) host)
((string-match-p (ha-eshell-host-regexp 'full) host) ;; Does it match any acceptable reference? Get the parts:
(string-match (ha-eshell-host-regexp 'full) host) ;; Why!? ((string-match-p (ha-eshell-host-regexp 'full) host)
(let* ((user1 (match-string 2 host)) (string-match (ha-eshell-host-regexp 'full) host) ;; Why!?
(host1 (match-string 3 host)) (let* ((user1 (match-string 2 host))
(user2 (match-string 6 host)) (host1 (match-string 3 host))
(host2 (match-string 7 host))) (user2 (match-string 6 host))
(if host1 (host2 (match-string 7 host)))
(ha-eshell-host->tramp user1 host1) (if host1
(ha-eshell-host->tramp user2 host2)))) (ha-eshell-host->tramp user1 host1)
(ha-eshell-host->tramp user2 host2))))
(t (format "/%s:" host))))) ;; Otherwise, we assume we have a hostname from a string?
;; Convert to a simple 'default' tramp URL:
(t (format "/%s:" host)))))
(eshell-there destination-path))) (eshell-there destination-path)))
#+END_SRC #+END_SRC
** Shell Here to There ** Shell Here to There
@ -709,21 +821,20 @@ The function to scan a line for hostname patterns uses different function calls
or Tramp references. This returns a tuple of the username (if or Tramp references. This returns a tuple of the username (if
found) and the hostname. found) and the hostname.
If a Tramp reference is found, the username part of the tuple If found a Tramp reference, the username part of the tuple is `nil'."
will be `nil'."
(save-excursion (save-excursion
(goto-char (line-beginning-position)) (goto-char (line-beginning-position))
(if (search-forward-regexp (ha-eshell-host-regexp 'tramp) (line-end-position) t) (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))) (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. ;; Returns the text associated with match expression, NUM or `nil' if found no match
(cl-flet ((ha-eshell-get-expression (num) (if-let ((first (match-beginning num)) (cl-flet ((ha-eshell-get-expression (num) (if-let ((first (match-beginning num))
(end (match-end num))) (end (match-end num)))
(buffer-substring-no-properties first end)))) (buffer-substring-no-properties first end))))
(search-forward-regexp (ha-eshell-host-regexp 'full) (line-end-position)) (search-forward-regexp (ha-eshell-host-regexp 'full) (line-end-position))
;; Until this is completely robust, let's keep this debugging code here: ;; Until robust, let's keep this debugging code here:
;; (message (mapconcat (lambda (tup) (if-let ((s (car tup)) ;; (message (mapconcat (lambda (tup) (if-let ((s (car tup))
;; (e (cadr tup))) ;; (e (cadr tup)))
;; (buffer-substring-no-properties s e) ;; (buffer-substring-no-properties s e)